@goatlab/fluent
Version:
Readable query Interface & API generator for TS and Node
334 lines (333 loc) • 11.8 kB
JavaScript
;
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;