import { MetadataInspector } from '@loopback/core' ; import { isBuiltinType , ModelDefinition , ModelMetadataHelper , Null , PropertyDefinition , PropertyType , RelationMetadata , resolveType , } from '@loopback/repository' ; import debugFactory from 'debug' ; import { inspect } from 'util' ; import { JsonSchema } from './index' ; import { JSON_SCHEMA_KEY } from './keys' ; const debug = debugFactory ( 'loopback:repository-json-schema:build-schema' ) ; export interface JsonSchemaOptions < T extends object > { /**
     * The title to use in the generated schema.
     *
     * When using options like `exclude`, the auto-generated title can be
     * difficult to read for humans. Use this option to change the title to
     * a more meaningful value.
     */ title ? : string ; /**
     * Set this flag if you want the schema to define navigational properties
     * for model relations.
     */ includeRelations ? : boolean ; example model property property property property model property partial ? : boolean | 'deep' ; /**
     * List of model properties to exclude from the schema.
     */ exclude ? : ( keyof T ) [ ] ; /**
     * List of model properties to mark as optional. Overrides the "partial"
     * option if it is not empty.
     */ optional ? : ( keyof T ) [ ] ; internal visited ? : { [ key ] : JsonSchema ; } ; } internal export function buildModelCacheKey < T extends object > ( options = { } ) { if ( Object . keys ( options ) . length === 0 ) { return 'modelOnly' ; } return 'model' + ( options . title ?? '' ) + getTitleSuffix ( options ) ; } param ctor export function getJsonSchema < T extends object > ( ctor , options ? ) { const cached = MetadataInspector . getClassMetadata ( JSON_SCHEMA_KEY , ctor , { ownMetadataOnly : true , } ) ; const key = buildModelCacheKey ( options ) ; let schema = cached ?. [ key ] ; if ( ! schema ) { schema = modelToJsonSchema ( ctor , options ) ; if ( cached ) { cached [ key ] = schema ; } else { MetadataInspector . defineMetadata ( JSON_SCHEMA_KEY . key , { [ key ] : schema } , ctor ) ; } } return schema ; } example param modelCtor param options export function getJsonSchemaRef < T extends object > ( modelCtor , options ? ) { const schemaWithDefinitions = getJsonSchema ( modelCtor , options ) ; const key = schemaWithDefinitions . title ; if ( ! key ) return schemaWithDefinitions ; const definitions = Object . assign ( { } , schemaWithDefinitions . definitions ) ; const schema = Object . assign ( { } , schemaWithDefinitions ) ; delete schema . definitions ; definitions [ key ] = schema ; return { $ref : `#/definitions/${ key }` , definitions , } ; } param type export function stringTypeToWrapper ( type ) { if ( typeof type === 'function' ) { return type ; } type = type . toLowerCase ( ) ; let wrapper ; switch ( type ) { case 'number' : { wrapper = Number ; break ; } case 'string' : { wrapper = String ; break ; } case 'boolean' : { wrapper = Boolean ; break ; } case 'array' : { wrapper = Array ; break ; } case 'object' : case 'any' : { wrapper = Object ; break ; } case 'date' : { wrapper = Date ; break ; } case 'buffer' : { wrapper = Buffer ; break ; } case 'null' : { wrapper = Null ; break ; } default : { throw new Error ( 'Unsupported type: ' + type ) ; } } return wrapper ; } param type export function isArrayType ( type ) { return type === Array || type === 'array' ; } param meta export function metaToJsonProperty ( meta ) { const propDef = { } ; let result ; let propertyType = meta . type as string | Function ; if ( isArrayType ( propertyType ) && meta . itemType ) { if ( isArrayType ( meta . itemType ) && ! meta . jsonSchema ) { throw new Error ( 'You must provide the "jsonSchema" field when define ' + 'a nested array property' ) ; } result = { type : 'array' , items : propDef } ; propertyType = meta . itemType as string | Function ; } else { result = propDef ; } const wrappedType = stringTypeToWrapper ( propertyType ) ; const resolvedType = resolveType ( wrappedType ) ; if ( resolvedType === Date ) { Object . assign ( propDef , { type : 'string' , format : 'date-time' , } ) ; } else if ( propertyType === 'any' ) { } else if ( isBuiltinType ( resolvedType ) ) { Object . assign ( propDef , { type : resolvedType . name . toLowerCase ( ) , } ) ; } else { Object . assign ( propDef , { $ref : `#/definitions/${ resolvedType . name }` } ) ; } if ( meta . description ) { Object . assign ( propDef , { description : meta . description , } ) ; } if ( meta . jsonSchema ) { Object . assign ( propDef , meta . jsonSchema ) ; } return result ; } param relMeta param targetRef export function getNavigationalPropertyForRelation ( relMeta , targetRef ) { if ( relMeta . targetsMany === true ) { return { type : 'array' , items : targetRef , } ; } else if ( relMeta . targetsMany === false ) { return targetRef ; } else { throw new Error ( `targetsMany attribute missing for ${ relMeta . name }` ) ; } } function buildSchemaTitle < T extends object > ( ctor , meta , options ) { if ( options . title ) return options . title ; const title = meta . title || ctor . name ; return title + getTitleSuffix ( options ) ; } param options function getTitleSuffix < T extends object > ( options = { } ) { let suffix = '' ; if ( options . optional ?. length ) { suffix += `Optional_${ options . optional . join ( '-' ) }_` ; } else if ( options . partial ) { suffix += 'Partial' ; } if ( options . exclude ?. length ) { suffix += `Excluding_${ options . exclude . join ( '-' ) }_` ; } if ( options . includeRelations ) { suffix += 'WithRelations' ; } return suffix ; } function stringifyOptions ( modelSettings = { } ) { return inspect ( modelSettings , { depth : Infinity , maxArrayLength : Infinity , breakLength : Infinity , } ) ; } function isEmptyJson ( obj ) { return ! ( obj && Object . keys ( obj ) . length ) ; } param typeName param options function getDescriptionSuffix < T extends object > ( typeName , rawOptions = { } ) { const options = { ... rawOptions } ; delete options . visited ; if ( options . optional && ! options . optional . length ) { delete options . optional ; } const type = typeName ; let tsType = type ; if ( options . includeRelations ) { tsType = `${ type }WithRelations` ; } if ( options . partial ) { tsType = `Partial<${ tsType }>` ; } if ( options . exclude ) { const excludedProps = options . exclude . map ( p => `'${ p }'` ) ; tsType = `Omit<${ tsType }, ${ excludedProps . join ( ' | ' ) }>` ; } if ( options . optional ) { const optionalProps = options . optional . map ( p => `'${ p }'` ) ; tsType = `@loopback/repository-json-schema#Optional<${ tsType }, ${ optionalProps . join ( ' | ' ) }>` ; } return ! isEmptyJson ( options ) ? `(tsType: ${ tsType }, schemaOptions: ${ stringifyOptions ( options ) })` : '' ; } param ctor export function modelToJsonSchema < T extends object > ( ctor , jsonSchemaOptions = { } ) { const options = { ... jsonSchemaOptions } ; options . visited = options . visited ?? { } ; options . optional = options . optional ?? [ ] ; const partial = options . partial && ! options . optional . length ; if ( options . partial && ! partial ) { debug ( 'Overriding "partial" option with "optional" option' ) ; delete options . partial ; } debug ( 'Creating schema for model %s' , ctor . name ) ; debug ( 'JSON schema options: %o' , options ) ; const modelDef = ModelMetadataHelper . getModelMetadata ( ctor ) ; if ( modelDef == null || Object . keys ( modelDef ) . length === 0 ) { return { } ; } const meta = modelDef as ModelDefinition ; debug ( 'Model settings' , meta . settings ) ; const title = buildSchemaTitle ( ctor , meta , options ) ; if ( options . visited [ title ] ) return options . visited [ title ] ; const result = { title } ; options . visited [ title ] = result ; result . type = 'object' ; const descriptionSuffix = getDescriptionSuffix ( ctor . name , options ) ; if ( meta . description ) { const formatSuffix = descriptionSuffix ? ` ${ descriptionSuffix }` : '' ; result . description = meta . description + formatSuffix ; } else if ( descriptionSuffix ) { result . description = descriptionSuffix ; } for ( const p in meta . properties ) { if ( options . exclude ?. includes ( p as keyof T ) ) { debug ( 'Property % is excluded by %s' , p , options . exclude ) ; continue ; } if ( meta . properties [ p ] . type == null ) { throw new Error ( `Property ${ ctor . name }.${ p } does not have "type" in its definition` ) ; } result . properties = result . properties ?? { } ; result . properties [ p ] = result . properties [ p ] || { } ; const metaProperty = Object . assign ( { } , meta . properties [ p ] ) ; result . properties [ p ] = metaToJsonProperty ( metaProperty ) ; const optional = options . optional . includes ( p as keyof T ) ; if ( metaProperty . required && ! ( partial || optional ) ) { result . required = result . required ?? [ ] ; result . required . push ( p ) ; } const resolvedType = resolveType ( metaProperty . type ) as string | Function ; const referenceType = isArrayType ( resolvedType ) ? resolveType ( metaProperty . itemType as string | Function ) : resolvedType ; if ( typeof referenceType !== 'function' || isBuiltinType ( referenceType ) ) { continue ; } const propOptions = { ... options } ; if ( propOptions . partial !== 'deep' ) { delete propOptions . partial ; } if ( propOptions . includeRelations === true ) { delete propOptions . includeRelations ; } delete propOptions . title ; delete propOptions . exclude ; const propSchema = getJsonSchema ( referenceType , propOptions ) ; if ( typeof result . properties [ p ] !== 'boolean' ) { const prop = result . properties [ p ] as JsonSchema ; const propTitle = propSchema . title ?? referenceType . name ; const targetRef = { $ref : `#/definitions/${ propTitle }` } ; if ( prop . type === 'array' && prop . items ) { prop . items = targetRef ; } else { result . properties [ p ] = targetRef ; } includeReferencedSchema ( propTitle , propSchema ) ; } } result . additionalProperties = meta . settings . strict === false ; debug ( '  additionalProperties?' , result . additionalProperties ) ; if ( options . includeRelations ) { for ( const r in meta . relations ) { result . properties = result . properties ?? { } ; const relMeta = meta . relations [ r ] ; const targetType = resolveType ( relMeta . target ) ; const targetOptions = { ... options } ; delete targetOptions . title ; const targetSchema = getJsonSchema ( targetType , targetOptions ) ; const targetRef = { $ref : `#/definitions/${ targetSchema . title }` } ; const propDef = getNavigationalPropertyForRelation ( relMeta , targetRef ) ; result . properties [ relMeta . name ] = result . properties [ relMeta . name ] || propDef ; includeReferencedSchema ( targetSchema . title ! , targetSchema ) ; } } function includeReferencedSchema ( name , schema ) { if ( ! schema || ! Object . keys ( schema ) . length ) return ; if ( result !== schema ?. definitions ) { for ( const key in schema . definitions ) { if ( key === title ) continue ; result . definitions = result . definitions ?? { } ; result . definitions [ key ] = schema . definitions [ key ] ; } delete schema . definitions ; } if ( result !== schema ) { result . definitions = result . definitions ?? { } ; result . definitions [ name ] = schema ; } } if ( meta . jsonSchema ) { Object . assign ( result , meta . jsonSchema ) ; } return result ; }