UNPKG

@goatlab/fluent

Version:

Readable query Interface & API generator for TS and Node

334 lines (333 loc) 11.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.modelToJsonSchema = exports.getNavigationalPropertyForRelation = exports.metaToJsonProperty = exports.isArrayType = exports.stringTypeToWrapper = exports.getJsonSchemaRef = exports.getJsonSchema = exports.buildModelCacheKey = exports.JSON_SCHEMA_KEY = void 0; const metadata_1 = require("@loopback/metadata"); const util_1 = require("util"); const type_resolver_1 = require("./type-resolver"); const metadata_2 = require("./metadata"); exports.JSON_SCHEMA_KEY = metadata_1.MetadataAccessor.create('loopback:json-schema'); const debug = console.log; function buildModelCacheKey(options = {}) { if (Object.keys(options).length === 0) { return 'modelOnly'; } return `model${options.title ?? ''}${getTitleSuffix(options)}`; } exports.buildModelCacheKey = buildModelCacheKey; function getJsonSchema(ctor, options) { const cached = metadata_1.MetadataInspector.getClassMetadata(exports.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 { metadata_1.MetadataInspector.defineMetadata(exports.JSON_SCHEMA_KEY.key, { [key]: schema }, ctor); } } return schema; } exports.getJsonSchema = getJsonSchema; function getJsonSchemaRef(modelCtor, options) { const schemaWithDefinitions = getJsonSchema(modelCtor, options); const key = schemaWithDefinitions.title; if (!key) return schemaWithDefinitions; const definitions = { ...schemaWithDefinitions.definitions }; const schema = { ...schemaWithDefinitions }; delete schema.definitions; definitions[key] = schema; return { $ref: `#/definitions/${key}`, definitions }; } exports.getJsonSchemaRef = getJsonSchemaRef; 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 = type_resolver_1.Null; break; } default: { throw new Error(`Unsupported type: ${type}`); } } return wrapper; } exports.stringTypeToWrapper = stringTypeToWrapper; function isArrayType(type) { return type === Array || type === 'array'; } exports.isArrayType = isArrayType; function metaToJsonProperty(meta) { const propDef = {}; let result; let propertyType = meta.type; 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; } else { result = propDef; } const wrappedType = stringTypeToWrapper(propertyType); const resolvedType = (0, type_resolver_1.resolveType)(wrappedType); if (resolvedType === Date) { Object.assign(propDef, { type: 'string', format: 'date-time' }); } else if (propertyType === 'any') { } else if ((0, type_resolver_1.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; } exports.metaToJsonProperty = metaToJsonProperty; function getNavigationalPropertyForRelation(relMeta, targetRef) { if (relMeta.targetsMany === true) { return { type: 'array', items: targetRef }; } if (relMeta.targetsMany === false) { return targetRef; } throw new Error(`targetsMany attribute missing for ${relMeta.name}`); } exports.getNavigationalPropertyForRelation = getNavigationalPropertyForRelation; function buildSchemaTitle(ctor, meta, options) { if (options.title) return options.title; const title = meta.title || ctor.name; return title + getTitleSuffix(options); } function getTitleSuffix(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 (0, util_1.inspect)(modelSettings, { depth: Infinity, maxArrayLength: Infinity, breakLength: Infinity }); } function isEmptyJson(obj) { return !(obj && Object.keys(obj).length); } function getDescriptionSuffix(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)})` : ''; } function modelToJsonSchema(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 = metadata_2.ModelMetadataHelper.getModelMetadata(ctor); if (modelDef == null || Object.keys(modelDef).length === 0) { return {}; } const meta = modelDef; 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)) { 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 = { ...meta.properties[p] }; result.properties[p] = metaToJsonProperty(metaProperty); const optional = options.optional.includes(p); if (metaProperty.required && !(partial || optional)) { result.required = result.required ?? []; result.required.push(p); } const resolvedType = (0, type_resolver_1.resolveType)(metaProperty.type); const referenceType = isArrayType(resolvedType) ? (0, type_resolver_1.resolveType)(metaProperty.itemType) : resolvedType; if (typeof referenceType !== 'function' || (0, type_resolver_1.isBuiltinType)(referenceType)) { continue; } const propOptions = { ...options }; if (propOptions.partial !== 'deep') { delete propOptions.partial; } if (propOptions.includeRelations === true) { delete propOptions.includeRelations; } delete propOptions.title; const propSchema = getJsonSchema(referenceType, propOptions); if (typeof result.properties[p] !== 'boolean') { const prop = result.properties[p]; 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 = (0, type_resolver_1.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; } exports.modelToJsonSchema = modelToJsonSchema;