UNPKG

@goatlab/fluent

Version:

Readable query Interface & API generator for TS and Node

449 lines 16 kB
"use strict"; // Copyright IBM Corp. 2018,2020. All Rights Reserved. // Node module: @loopback/repository-json-schema // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT Object.defineProperty(exports, "__esModule", { value: true }); exports.JSON_SCHEMA_KEY = void 0; exports.buildModelCacheKey = buildModelCacheKey; exports.getJsonSchema = getJsonSchema; exports.getJsonSchemaRef = getJsonSchemaRef; exports.stringTypeToWrapper = stringTypeToWrapper; exports.isArrayType = isArrayType; exports.metaToJsonProperty = metaToJsonProperty; exports.getNavigationalPropertyForRelation = getNavigationalPropertyForRelation; exports.modelToJsonSchema = modelToJsonSchema; const node_util_1 = require("node:util"); const metadata_1 = require("@loopback/metadata"); const metadata_2 = require("./metadata"); const type_resolver_1 = require("./type-resolver"); exports.JSON_SCHEMA_KEY = metadata_1.MetadataAccessor.create('loopback:json-schema'); const debug = console.log; /** * @internal */ function buildModelCacheKey(options = {}) { // Backwards compatibility: preserve cache key "modelOnly" if (Object.keys(options).length === 0) { return 'modelOnly'; } // New key schema: use the same suffix as we use for schema title // For example: "modelPartialWithRelations" // Note this new key schema preserves the old key "modelWithRelations" return `model${options.title ?? ''}${getTitleSuffix(options)}`; } /** * Gets the JSON Schema of a TypeScript model/class by seeing if one exists * in a cache. If not, one is generated and then cached. * @param ctor - Constructor of class to get JSON Schema from */ function getJsonSchema(ctor, options) { // In the near future the metadata will be an object with // different titles as keys const cached = metadata_1.MetadataInspector.getClassMetadata(exports.JSON_SCHEMA_KEY, ctor, { ownMetadataOnly: true }); const key = buildModelCacheKey(options); let schema = cached?.[key]; if (!schema) { // Create new json schema from model // if not found in cache for specific key schema = modelToJsonSchema(ctor, options); if (cached) { // Add a new key to the cached schema of the model cached[key] = schema; } else { // Define new metadata and set in cache metadata_1.MetadataInspector.defineMetadata(exports.JSON_SCHEMA_KEY.key, { [key]: schema }, ctor); } } return schema; } /** * Describe the provided Model as a reference to a definition shared by multiple * endpoints. The definition is included in the returned schema. * * @example * * ```ts * const schema = { * $ref: '/definitions/Product', * definitions: { * Product: { * title: 'Product', * properties: { * // etc. * } * } * } * } * ``` * * @param modelCtor - The model constructor (e.g. `Product`) * @param options - Additional options */ function getJsonSchemaRef(modelCtor, options) { const schemaWithDefinitions = getJsonSchema(modelCtor, options); const key = schemaWithDefinitions.title; // ctor is not a model if (!key) { return schemaWithDefinitions; } const definitions = { ...schemaWithDefinitions.definitions }; const schema = { ...schemaWithDefinitions }; schema.definitions = undefined; definitions[key] = schema; return { $ref: `#/definitions/${key}`, definitions }; } /** * Gets the wrapper function of primitives string, number, and boolean * @param type - Name of type */ function stringTypeToWrapper(type) { if (typeof type === 'function') { return type; } const lowerType = type.toLowerCase(); let wrapper; switch (lowerType) { 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; } /** * Determines whether a given string or constructor is array type or not * @param type - Type as string or wrapper */ function isArrayType(type) { return type === Array || type === 'array'; } /** * Converts property metadata into a JSON property definition * @param meta */ 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') { // no-op, the json schema for any type is {} } 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; } /** * Checks and return navigational property definition for the relation * @param relMeta Relation metadata object * @param targetRef Schema definition for the target model */ function getNavigationalPropertyForRelation(relMeta, targetRef) { if (relMeta.targetsMany === true) { // Targets an array of object, like, hasMany return { type: 'array', items: targetRef }; } if (relMeta.targetsMany === false) { // Targets single object, like, hasOne, belongsTo return targetRef; } // targetsMany is undefined or null // not allowed if includeRelations is true throw new Error(`targetsMany attribute missing for ${relMeta.name}`); } function buildSchemaTitle(ctor, meta, options) { if (options.title) { return options.title; } const title = meta.title || ctor.name; return title + getTitleSuffix(options); } /** * Checks the options and generates a descriptive suffix using compatible chars * @param options json schema 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, node_util_1.inspect)(modelSettings, { depth: Number.POSITIVE_INFINITY, maxArrayLength: Number.POSITIVE_INFINITY, breakLength: Number.POSITIVE_INFINITY }); } function isEmptyJson(obj) { return !(obj && Object.keys(obj).length); } /** * Checks the options and generates a descriptive suffix that contains the * TypeScript type and options * @param typeName - TypeScript's type name * @param options - json schema options */ function getDescriptionSuffix(typeName, rawOptions = {}) { const options = { ...rawOptions }; options.visited = undefined; if (options.optional && !options.optional.length) { options.optional = undefined; } 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 => `'${String(p)}'`); tsType = `Omit<${tsType}, ${excludedProps.join(' | ')}>`; } if (options.optional) { const optionalProps = options.optional.map(p => `'${String(p)}'`); tsType = `@loopback/repository-json-schema#Optional<${tsType}, ${optionalProps.join(' | ')}>`; } return !isEmptyJson(options) ? `(tsType: ${tsType}, schemaOptions: ${stringifyOptions(options)})` : ''; } // NOTE(shimks) no metadata for: union, optional, nested array, any, enum, // string literal, anonymous types, and inherited properties /** * Converts a TypeScript class into a JSON Schema using TypeScript's * reflection API * @param ctor - Constructor of class to convert from */ 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'); options.partial = undefined; } debug('Creating schema for model %s', ctor.name); debug('JSON schema options: %o', options); const modelDef = metadata_2.ModelMetadataHelper.getModelMetadata(ctor); // returns an empty object if metadata is an empty object 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) { // Circular import of model classes can lead to this situation 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] }; if (!metaProperty.type) { throw new Error(`Property ${p} does not have a type`); } // populating "properties" key result.properties[p] = metaToJsonProperty(metaProperty); // handling 'required' metadata const optional = options.optional.includes(p); if (metaProperty.required && !(partial || optional)) { result.required = result.required ?? []; result.required.push(p); } // populating JSON Schema 'definitions' // shimks: ugly type casting; this should be replaced by logic to throw // error if itemType/type is not a string or a function const resolvedType = (0, type_resolver_1.resolveType)(metaProperty.type); const referenceType = isArrayType(resolvedType) ? // shimks: ugly type casting; this should be replaced by logic to throw // error if itemType/type is not a string or a function (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') { // Do not cascade `partial` to nested properties propOptions.partial = undefined; } if (propOptions.includeRelations === true) { // Do not cascade `includeRelations` to nested properties propOptions.includeRelations = undefined; } // `title` is the unique identity of a schema, // it should be removed from the `options` // when generating the relation or property schemas propOptions.title = undefined; const propSchema = getJsonSchema(referenceType, propOptions); // JSONSchema6Definition allows both boolean and JSONSchema6 types 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) { // Update $ref for array type 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]; if (!relMeta) { continue; } const targetType = (0, type_resolver_1.resolveType)(relMeta.target); // `title` is the unique identity of a schema, // it should be removed from the `options` // when generating the relation or property schemas const targetOptions = { ...options }; targetOptions.title = undefined; 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; } // promote nested definition to the top level if (result !== schema?.definitions) { for (const key in schema.definitions) { if (key === title) { continue; } result.definitions = result.definitions ?? {}; result.definitions[key] = schema.definitions[key]; } schema.definitions = undefined; } if (result !== schema) { result.definitions = result.definitions ?? {}; result.definitions[name] = schema; } } if (meta.jsonSchema) { Object.assign(result, meta.jsonSchema); } return result; } //# sourceMappingURL=build-schema.js.map