import mongoose , { Document } from 'mongoose' ; import type { Schema , Model } from 'mongoose' ; import type { SchemaComposer , ObjectTypeComposer , EnumTypeComposer , ComposeOutputTypeDefinition , ObjectTypeComposerFieldConfigAsObjectDefinition , } from 'graphql-compose' ; import { upperFirst } from 'graphql-compose' ; import type { GraphQLScalarType } from 'graphql-compose/lib/graphql' ; import GraphQLMongoID from './types/MongoID' ; import GraphQLBSONDecimal from './types/BSONDecimal' ; type MongooseFieldT = { path ? : string ; instance : string ; caster ? : any ; options ? : { description ? : string ; alias ? : string ; } ; originalRequiredValue ? : string | ( ( ) => any ) ; isRequired ? : boolean ; defaultValue ? : any ; enumValues ? : string [ ] ; schema ? : Schema ; _index ? : { [ optionName ] : any ; } ; } ; type MongooseFieldMapT = { [ fieldName ] : MongooseFieldT ; } ; type ComposeScalarType = string | GraphQLScalarType ; export type MongoosePseudoModelT = { schema : Schema < any > ; } ; export enum ComplexTypes { ARRAY = 'ARRAY' , EMBEDDED = 'EMBEDDED' , DOCUMENT_ARRAY = 'DOCUMENT_ARRAY' , ENUM = 'ENUM' , REFERENCE = 'REFERENCE' , SCALAR = 'SCALAR' , MIXED = 'MIXED' , DECIMAL = 'DECIMAL' } const SubdocumentTypeClass = ( mongoose . Schema . Types as any ) ?. Embedded || ( mongoose . Schema . Types as any ) . Subdocument ; function _getFieldName ( field ) { return field . path || '__unknownField__' ; } function _getFieldType ( field ) { return field . instance ; } function _getFieldDescription ( field ) { if ( field . options && field . options . description ) { return field . options . description ; } return undefined ; } function _getFieldEnums ( field ) { if ( field . enumValues && field . enumValues . length > 0 ) { return field . enumValues ; } return undefined ; } export function dotPathsToEmbedded ( fields ) { const result = { } ; Object . keys ( fields ) . forEach ( ( fieldName ) => { const dotIdx = fieldName . indexOf ( '.' ) ; if ( dotIdx === - 1 ) { result [ fieldName ] = fields [ fieldName ] ; } else if ( fieldName . substr ( dotIdx , 3 ) === '.$*' ) { } else { const name = fieldName . substr ( 0 , dotIdx ) ; if ( ! result [ name ] ) { const embeddedField = { instance : 'Embedded' , path : name , schema : { paths : { } , } , } as MongooseFieldT ; result [ name ] = embeddedField ; } const subName = fieldName . substr ( dotIdx + 1 ) ; const fieldSchema = result [ name ] . schema ; if ( ! fieldSchema ) { throw new Error ( `Field ${ name } does not have schema property` ) ; } fieldSchema . paths [ subName ] = { ... fields [ fieldName ] , path : subName } as any ; } } ) ; return result ; } export function getFieldsFromModel ( model ) { if ( ! model || ! model . schema || ! model . schema . paths ) { throw new Error ( 'You provide incorrect mongoose model to `getFieldsFromModel()`. ' + 'Correct model should contain `schema.paths` properties.' ) ; } const fields = { } ; const paths = dotPathsToEmbedded ( ( model as any ) . schema . paths ) ; Object . keys ( paths ) . filter ( ( path ) => ! path . startsWith ( '__' ) ) . forEach ( ( path ) => { fields [ path ] = paths [ path ] ; } ) ; return fields ; } export function convertModelToGraphQL < TDoc extends Document , TContext > ( model , typeName , schemaComposer ) { const sc = schemaComposer ; if ( ! typeName ) { throw new Error ( 'You provide empty name for type. `name` argument should be non-empty string.' ) ; } if ( sc . has ( model . schema ) ) { return sc . getOTC ( model . schema ) ; } const typeComposer = sc . getOrCreateOTC ( typeName ) ; sc . set ( model . schema , typeComposer ) ; sc . set ( typeName , typeComposer ) ; const mongooseFields = getFieldsFromModel ( model ) ; const graphqlFields = { } as Record < string , ObjectTypeComposerFieldConfigAsObjectDefinition < any , any > > ; const requiredFields = [ ] as string [ ] ; Object . keys ( mongooseFields ) . forEach ( ( key ) => { const mongooseField = mongooseFields [ key ] ; let fieldName = key ; if ( typeof mongooseField ?. options ?. alias === 'string' ) { fieldName = mongooseField ?. options ?. alias ; } if ( mongooseField . isRequired && typeof mongooseField ?. originalRequiredValue !== 'function' ) { requiredFields . push ( fieldName ) ; } let type = convertFieldToGraphQL ( mongooseField , typeName , sc ) ; if ( fieldName === '_id' && type === 'Float' ) { type = 'Int' ; } graphqlFields [ fieldName ] = { type , description : _getFieldDescription ( mongooseField ) , } ; if ( mongooseField ?. schema && ( mongooseField as any ) ?. $isSingleNested ) { graphqlFields [ fieldName ] . extensions = { isSingleNestedMongooseSchema : true , } ; } if ( mongooseField ?. defaultValue !== null && mongooseField ?. defaultValue !== undefined ) { if ( ! graphqlFields [ fieldName ] . extensions ) graphqlFields [ fieldName ] . extensions = { } ; ( graphqlFields as any ) [ fieldName ] . extensions . defaultValue = mongooseField ?. defaultValue ; } } ) ; typeComposer . addFields ( graphqlFields ) ; typeComposer . makeFieldNonNull ( requiredFields ) ; return typeComposer ; } export function convertSchemaToGraphQL ( schema , typeName , schemaComposer ) { const sc = schemaComposer ; if ( ! typeName ) { throw new Error ( 'You provide empty name for type. `name` argument should be non-empty string.' ) ; } if ( sc . has ( schema ) ) { return sc . getOTC ( schema ) ; } const tc = convertModelToGraphQL ( { schema } , typeName , sc ) ; tc . getInputTypeComposer ( ) ; sc . set ( schema , tc ) ; return tc ; } export function convertFieldToGraphQL ( field , prefix = '' , schemaComposer ) { if ( ! schemaComposer . has ( 'MongoID' ) ) { schemaComposer . add ( GraphQLMongoID ) ; } const complexType = deriveComplexType ( field ) ; switch ( complexType ) { case ComplexTypes . SCALAR : return scalarToGraphQL ( field ) ; case ComplexTypes . ARRAY : return arrayToGraphQL ( field , prefix , schemaComposer ) ; case ComplexTypes . EMBEDDED : return embeddedToGraphQL ( field , prefix , schemaComposer ) ; case ComplexTypes . ENUM : return enumToGraphQL ( field , prefix , schemaComposer ) ; case ComplexTypes . REFERENCE : return referenceToGraphQL ( field ) ; case ComplexTypes . DOCUMENT_ARRAY : return documentArrayToGraphQL ( field , prefix , schemaComposer ) ; case ComplexTypes . MIXED : return 'JSON' ; case ComplexTypes . DECIMAL : if ( ! schemaComposer . has ( 'BSONDecimal' ) ) { schemaComposer . add ( GraphQLBSONDecimal ) ; } return 'BSONDecimal' ; default : return scalarToGraphQL ( field ) ; } } export function deriveComplexType ( field ) { if ( ! field || ! field . path || ! field . instance ) { throw new Error ( 'You provide incorrect mongoose field to `deriveComplexType()`. ' + 'Correct field should contain `path` and `instance` properties.' ) ; } const fieldType = _getFieldType ( field ) ; if ( field instanceof mongoose . Schema . Types . DocumentArray || ( fieldType === 'Array' && field ?. schema ?. paths ) ) { return ComplexTypes . DOCUMENT_ARRAY ; } else if ( field instanceof SubdocumentTypeClass || fieldType === 'Embedded' ) { return ComplexTypes . EMBEDDED ; } else if ( field instanceof mongoose . Schema . Types . Array || field ?. caster ?. instance ) { return ComplexTypes . ARRAY ; } else if ( field instanceof mongoose . Schema . Types . Mixed ) { return ComplexTypes . MIXED ; } else if ( fieldType === 'ObjectID' ) { return ComplexTypes . REFERENCE ; } else if ( fieldType === 'Decimal128' ) { return ComplexTypes . DECIMAL ; } const enums = _getFieldEnums ( field ) ; if ( enums ) { return ComplexTypes . ENUM ; } return ComplexTypes . SCALAR ; } export function scalarToGraphQL ( field ) { const typeName = _getFieldType ( field ) ; switch ( typeName ) { case 'String' : return 'String' ; case 'Number' : return 'Float' ; case 'Date' : return 'Date' ; case 'Buffer' : return 'Buffer' ; case 'Boolean' : return 'Boolean' ; case 'ObjectID' : return 'MongoID' ; default : return 'JSON' ; } } export function arrayToGraphQL ( field , prefix = '' , schemaComposer ) { if ( ! field || ! field . caster ) { throw new Error ( 'You provide incorrect mongoose field to `arrayToGraphQL()`. ' + 'Correct field should contain `caster` property.' ) ; } const unwrappedField = { ... field . caster } ; const outputType = convertFieldToGraphQL ( unwrappedField , prefix , schemaComposer ) ; return [ outputType ] ; } export function embeddedToGraphQL ( field , prefix = '' , schemaComposer ) { const fieldName = _getFieldName ( field ) ; const fieldType = _getFieldType ( field ) ; if ( fieldType !== 'Embedded' ) { throw new Error ( `You provide incorrect field '${ prefix }.${ fieldName }' to 'embeddedToGraphQL()'. ` + 'This field should has `Embedded` type. ' ) ; } const fieldSchema = field . schema ; if ( ! fieldSchema ) { throw new Error ( `Mongoose field '${ prefix }.${ fieldName }' should have 'schema' property` ) ; } const typeName = `${ prefix }${ upperFirst ( fieldName ) }` ; return convertSchemaToGraphQL ( fieldSchema , typeName , schemaComposer ) ; } export function enumToGraphQL ( field , prefix = '' , schemaComposer ) { const valueList = _getFieldEnums ( field ) ; if ( ! valueList ) { throw new Error ( 'You provide incorrect mongoose field to `enumToGraphQL()`. ' + 'Correct field should contain `enumValues` property' ) ; } const typeName = `Enum${ prefix }${ upperFirst ( _getFieldName ( field ) ) }` ; return schemaComposer . getOrCreateETC ( typeName , ( etc ) => { const desc = _getFieldDescription ( field ) ; if ( desc ) etc . setDescription ( desc ) ; const fields = valueList . reduce ( ( result , value ) => { let key ; if ( value === null ) { key = 'NULL' ; } else if ( value === '' ) { key = 'EMPTY_STRING' ; } else { key = value . replace ( /[^_a-zA-Z0-9]/g , '_' ) . replace ( /(^[0-9])(.*)/g , 'a_$1$2' ) ; } result [ key ] = { value } ; return result ; } , { } as Record < string , { value : any ; } > ) ; etc . setFields ( fields ) ; } ) ; } export function documentArrayToGraphQL ( field , prefix = '' , schemaComposer ) { if ( ! ( field instanceof mongoose . Schema . Types . DocumentArray ) && ! field ?. schema ?. paths ) { throw new Error ( 'You provide incorrect mongoose field to `documentArrayToGraphQL()`. ' + 'Correct field should be instance of `mongoose.Schema.Types.DocumentArray`' ) ; } const typeName = `${ prefix }${ upperFirst ( _getFieldName ( field ) ) }` ; const tc = convertModelToGraphQL ( field as any , typeName , schemaComposer ) ; return [ tc ] ; } export function referenceToGraphQL ( field ) { return scalarToGraphQL ( field ) ; }