import type * as core from '@contentlayer/core' ; import type { PosixFilePath } from '@contentlayer/utils' ; import { filePathJoin } from '@contentlayer/utils' ; import { O , OT , pipe , T , These } from '@contentlayer/utils/effect' ; import { fs } from '@contentlayer/utils/node' ; import minimatch from 'minimatch' ; import { FetchDataError } from '../errors/index.js' ; import type { FilePathPatternMap } from '../index.js' ; import type { HasDocumentTypeMapState } from './DocumentTypeMap.js' ; import { DocumentTypeMapState } from './DocumentTypeMap.js' ; import type { RawContent } from './types.js' ; type ValidateDocumentDataError = FetchDataError . CouldNotDetermineDocumentTypeError | FetchDataError . NoSuchDocumentTypeError | FetchDataError . MissingRequiredFieldsError | FetchDataError . ExtraFieldDataError | FetchDataError . ReferencedFileDoesNotExistError | FetchDataError . IncompatibleFieldDataError ; export const validateDocumentData = ( { coreSchemaDef , rawContent , relativeFilePath , filePathPatternMap , options , contentDirPath , } ) : T . Effect < HasDocumentTypeMapState & OT . HasTracer , never , These . These < ValidateDocumentDataError , { documentTypeDef : core . DocumentTypeDef ; } > > => pipe ( T . gen ( function * ( $ ) { const documentDefName = getDocumentDefName ( { rawContent , filePathPatternMap , relativeFilePath , options } ) ; yield * $ ( OT . addAttribute ( 'documentDefName' , documentDefName ! ) ) ; if ( documentDefName === undefined ) { const typeFieldName = options . fieldOptions . typeFieldName ; return These . fail ( new FetchDataError . CouldNotDetermineDocumentTypeError ( { documentFilePath : relativeFilePath , typeFieldName } ) ) ; } const documentTypeDef = coreSchemaDef . documentTypeDefMap [ documentDefName ] ; if ( documentTypeDef === undefined ) { return These . fail ( new FetchDataError . NoSuchDocumentTypeError ( { documentTypeName : documentDefName , documentFilePath : relativeFilePath , } ) ) ; } yield * $ ( DocumentTypeMapState . update ( ( _ ) => _ . add ( documentDefName , relativeFilePath ) ) ) ; const existingDataFieldKeys = Object . keys ( rawContent . fields ) ; const requiredFieldsWithoutDefaultValue = documentTypeDef . fieldDefs . filter ( ( _ ) => _ . isRequired && _ . default === undefined && _ . isSystemField === false ) ; const misingRequiredFieldDefs = requiredFieldsWithoutDefaultValue . filter ( ( fieldDef ) => ! existingDataFieldKeys . includes ( fieldDef . name ) ) ; if ( misingRequiredFieldDefs . length > 0 ) { return These . fail ( new FetchDataError . MissingRequiredFieldsError ( { documentFilePath : relativeFilePath , documentTypeName : documentTypeDef . name , fieldDefsWithMissingData : misingRequiredFieldDefs , } ) ) ; } let warningOption = O . none ; const typeFieldName = options . fieldOptions . typeFieldName ; const schemaFieldNames = documentTypeDef . fieldDefs . map ( ( _ ) => _ . name ) . concat ( [ typeFieldName ] ) ; const extraFieldEntries = existingDataFieldKeys . filter ( ( fieldKey ) => ! schemaFieldNames . includes ( fieldKey ) ) . map ( ( fieldKey ) => [ fieldKey , rawContent . fields [ fieldKey ] ] as const ) ; if ( extraFieldEntries . length > 0 ) { const extraFieldDataError = new FetchDataError . ExtraFieldDataError ( { documentFilePath : relativeFilePath , extraFieldEntries , documentTypeName : documentTypeDef . name , } ) ; warningOption = O . some ( extraFieldDataError ) ; } for ( const fieldDef of documentTypeDef . fieldDefs ) { const fieldValidOption = yield * $ ( validateFieldData ( { documentFilePath : relativeFilePath , documentTypeName : documentTypeDef . name , fieldDef , rawFieldData : rawContent . fields [ fieldDef . name ] , contentDirPath , } ) ) ; if ( O . isSome ( fieldValidOption ) ) { return These . fail ( fieldValidOption . value ) ; } } return These . warnOption ( { documentTypeDef } , warningOption ) ; } ) , OT . withSpan ( 'validateDocumentData' , { attributes : { relativeFilePath } } ) ) ; const getDocumentDefName = ( { rawContent , relativeFilePath , filePathPatternMap , options , } ) : string | undefined => { const typeFieldName = options . fieldOptions . typeFieldName ; const typeFieldValue = rawContent . fields [ typeFieldName ] ; if ( typeFieldValue !== undefined ) { return typeFieldValue ; } return getDocumentDefNameByFilePathPattern ( { filePathPatternMap , relativeFilePath } ) ; } ; const getDocumentDefNameByFilePathPattern = ( { relativeFilePath , filePathPatternMap , } ) : string | undefined => { const matchingFilePathPattern = Object . keys ( filePathPatternMap ) . find ( ( filePathPattern ) => minimatch ( relativeFilePath , filePathPattern ) ) ; if ( matchingFilePathPattern ) { return filePathPatternMap [ matchingFilePathPattern ] ; } return undefined ; } ; const validateFieldData = ( { fieldDef , rawFieldData , documentFilePath , documentTypeName , contentDirPath , } ) : T . Effect < OT . HasTracer , never , O . Option < FetchDataError . IncompatibleFieldDataError | FetchDataError . ReferencedFileDoesNotExistError > > => T . gen ( function * ( $ ) { const dataIsNil = rawFieldData === undefined || rawFieldData === null ; if ( dataIsNil && fieldDef . isRequired === false ) { return O . none ; } switch ( fieldDef . type ) { case 'list' : return Array . isArray ( rawFieldData ) ? O . none : O . some ( new FetchDataError . IncompatibleFieldDataError ( { incompatibleFieldData : [ [ fieldDef . name , rawFieldData ] ] , documentFilePath , documentTypeName , } ) ) ; case 'reference' : if ( typeof rawFieldData === 'string' ) { const fullFilePath = filePathJoin ( contentDirPath , rawFieldData ) ; const fileExists = yield * $ ( fs . fileOrDirExists ( fullFilePath ) ) ; if ( ! fileExists ) { return O . some ( new FetchDataError . ReferencedFileDoesNotExistError ( { referencedFilePath : rawFieldData as any , fieldName : fieldDef . name , documentFilePath , documentTypeName , } ) ) ; } } default : return O . none ; } } ) [ '|>' ] ( T . orDie ) ;