@allgemein/schema-api
Version:
Library for schema api
501 lines • 19.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonSchema7Serializer = void 0;
const lodash_1 = require("lodash");
const base_1 = require("@allgemein/base");
const JsonSchema7_1 = require("./JsonSchema7");
const Constants_1 = require("../Constants");
const IClassRef_1 = require("../../api/IClassRef");
const SchemaUtils_1 = require("../SchemaUtils");
const IEntityRef_1 = require("../../api/IEntityRef");
const Constants_2 = require("./Constants");
const MetadataRegistry_1 = require("../registry/MetadataRegistry");
const functions_1 = require("./functions");
const functions_2 = require("../functions");
class JsonSchema7Serializer {
constructor(opts) {
this.options = opts || {};
(0, lodash_1.defaults)(this.options, {
keysToSkip: Constants_1.DEFAULT_KEY_TO_SKIP,
ignoreUnknownType: false,
defaultTypeHint: Constants_1.T_STRING,
[Constants_2.C_ONLY_DECORATED]: false,
postProcess: (src, dst, serializer) => {
if (src && src.metaType === Constants_1.METATYPE_PROPERTY && (0, lodash_1.has)(dst, 'cardinality')) {
const cardinality = dst['cardinality'];
delete dst['cardinality'];
// TODO handle value corretly minItem/maxItem when item!
if (cardinality === 0) {
}
else {
//delete dst['cardinality'];
}
}
}
});
this.data = this.getOrCreateSchemaDefinitions();
}
uri() {
return `http://json-schema.org/${Constants_2.DRAFT_07}/schema`;
}
isCurrentClass(x) {
return (0, lodash_1.snakeCase)(this.current) === (0, lodash_1.snakeCase)(x);
}
serialize(klass) {
if ((0, lodash_1.isFunction)(klass)) {
this.current = base_1.ClassUtils.getClassName(klass);
this.describeClass(klass);
}
else if ((0, IClassRef_1.isClassRef)(klass)) {
this.current = klass.name;
this.describeClassRef(klass);
}
else if ((0, IEntityRef_1.isEntityRef)(klass)) {
this.current = klass.name;
this.describeEntityRef(klass);
}
else {
throw new base_1.NotSupportedError('class or function can\'t be used');
}
return this.getJsonSchema();
}
getJsonSchema() {
return this.data;
}
getOrCreateSchemaDefinitions(schema) {
if (!schema) {
schema = {
$schema: this.uri() + '#',
definitions: {}
};
}
else if (schema && !schema.definitions) {
schema.definitions = {};
}
return schema;
}
getOrCreateRoot(entityName, klass) {
if (this.data.definitions[entityName]) {
// definition is present and
this.applyRef(entityName);
return null;
}
const className = (0, functions_2.getClassName)(klass);
const appendTarget = this.isAppendTargetSet();
const root = this.data.definitions[entityName] = {
title: className ? className : entityName,
type: Constants_1.T_OBJECT,
};
if ((0, IEntityRef_1.isEntityRef)(klass)) {
// when an entity mark with $id!
root.$id = '#' + klass.name;
}
else if ((0, IClassRef_1.isClassRef)(klass) && klass.hasEntityRef()) {
root.$id = '#' + klass.getEntityRef().name;
}
if (((0, IEntityRef_1.isEntityRef)(klass) || (0, IClassRef_1.isClassRef)(klass))) {
if ((0, lodash_1.get)(this.options, 'appendNamespace', false)) {
(0, lodash_1.set)(root, '$' + Constants_1.METATYPE_NAMESPACE, klass.getRegistry().getLookupRegistry().getNamespace());
}
const data = klass.getOptions();
this.appendAdditionalOptions(root, data);
}
if (appendTarget) {
root.$target = SchemaUtils_1.SchemaUtils.getFunction(klass);
}
this.applyRef(entityName);
if (this.options.postProcess) {
this.options.postProcess(klass, root, this);
}
return root;
}
applyRef(className) {
if (!this.data.$ref && !this.data.anyOf) {
this.data.$ref = '#/definitions/' + className;
}
else if (this.data.$ref && !this.data.anyOf) {
if (this.isCurrentClass(className)) {
this.data.anyOf = [
{ $ref: this.data.$ref },
{ $ref: '#/definitions/' + className }
];
delete this.data['$ref'];
}
}
else if (!this.data.$ref && this.data.anyOf) {
if (this.isCurrentClass(className)) {
this.data.anyOf.push({ $ref: '#/definitions/' + className });
}
}
if (this.data.anyOf) {
this.data.anyOf = (0, lodash_1.uniqBy)(this.data.anyOf, x => JSON.stringify(x));
}
}
describeEntityRef(klass) {
return this.describeRef(klass);
}
describeClassRef(klass) {
return this.describeRef(klass);
}
describeRef(klass) {
const clsRef = (0, IEntityRef_1.isEntityRef)(klass) ? klass.getClassRef() : klass;
const className = (0, IEntityRef_1.isEntityRef)(klass) ? klass.name : clsRef.name;
const root = this.getOrCreateRoot(className, klass);
if (!root) {
return null;
}
const rootProps = this.describePropertiesForRef(clsRef);
this.appendProperties(root, rootProps);
const proto = clsRef.getExtend();
if (proto) {
this.describeInheritedClass(root, proto);
}
return root;
}
describeInheritedClass(root, proto) {
const inheritedClassName = proto.name;
root.allOf = [{ $ref: '#/definitions/' + inheritedClassName }];
if (!this.data.definitions[inheritedClassName]) {
const inheritedClass = this.getOrCreateRoot(inheritedClassName, proto);
const props = this.describePropertiesForRef(proto);
this.appendProperties(inheritedClass, props);
}
}
describeClass(klass) {
const className = base_1.ClassUtils.getClassName(klass);
const root = this.getOrCreateRoot(className, klass);
if (!root) {
return null;
}
const rootProps = this.describePropertiesForFunction(klass);
this.appendProperties(root, rootProps);
const proto = SchemaUtils_1.SchemaUtils.getInherited(klass);
if (proto) {
const inheritedClassName = base_1.ClassUtils.getClassName(proto);
root.allOf = [{ $ref: '#/definitions/' + inheritedClassName }];
if (this.data && !this.data.definitions[inheritedClassName]) {
const inheritedClass = this.getOrCreateRoot(inheritedClassName, proto);
const props = this.describePropertiesForFunction(proto);
this.appendProperties(inheritedClass, props);
}
}
return root;
}
appendProperties(data, properties) {
data.properties = {};
for (const k of (0, lodash_1.keys)(properties)) {
const p = properties[k];
if (p[Constants_1.K_PATTERN_PROPERTY]) {
if (!data.patternProperties) {
data.patternProperties = {};
}
data.patternProperties[k] = p;
}
else {
data.properties[k] = p;
}
delete p[Constants_1.K_PATTERN_PROPERTY];
}
}
describePropertiesForFunction(klass) {
const properties = {};
let _properties = [];
let instance = null;
try {
instance = Reflect.construct(klass, []);
_properties = Reflect.ownKeys(instance);
}
catch (e) {
}
for (const p of _properties) {
if ((0, lodash_1.isString)(p)) {
if (!(0, lodash_1.get)(this.options, Constants_2.C_ONLY_DECORATED, false) && this.allowed(p, klass)) {
const result = this.describePropertyForFunction(klass, p, instance);
properties[p] = result;
}
}
}
return properties;
}
describePropertiesForRef(klass) {
const properties = {};
for (const prop of klass.getPropertyRefs()) {
if (this.allowed(prop)) {
const result = this.describePropertyForRef(klass, prop);
if (result) {
properties[prop.name] = result;
}
}
}
const instance = klass.create(false);
// TODO own key
const _properties = Reflect.ownKeys(instance);
for (const p of _properties) {
if ((0, lodash_1.isString)(p) && !(0, lodash_1.has)(properties, p)) {
if (!(0, lodash_1.get)(this.options, Constants_2.C_ONLY_DECORATED, false)) {
const result = this.describePropertyForFunction(klass, p, instance);
if (result) {
properties[p] = result;
}
}
}
else if ((0, lodash_1.isString)(p) && (0, lodash_1.has)(properties, p)) {
const value = instance[p];
if (!((0, lodash_1.isNull)(value) || (0, lodash_1.isUndefined)(value))) {
// add default value if exists
properties[p].default = SchemaUtils_1.SchemaUtils.normValue(value);
}
}
}
return properties;
}
isAppendTargetSet() {
return (0, lodash_1.get)(this.options, 'appendTarget', false);
}
describePropertyForFunction(klass, propertyName, instance) {
const clazz = (0, IClassRef_1.isClassRef)(klass) ? klass.getClass() : klass;
const propMeta = {};
// check if property is object
let value = instance ? instance[propertyName] : undefined;
let typeHint = typeof value;
const reflectMetadataType = (0, functions_1.getReflectedType)(clazz, propertyName);
if (this.options.typeHint && (0, lodash_1.isFunction)(this.options.typeHint)) {
typeHint = this.options.typeHint(klass, propertyName, instance, value);
}
else {
if (typeHint === Constants_1.T_OBJECT) {
if (reflectMetadataType && reflectMetadataType !== Constants_1.T_OBJECT) {
typeHint = reflectMetadataType;
}
else {
if (value === null || value === undefined) {
typeHint = this.getDefaultTypeHint();
}
else if (value === false || value === true) {
typeHint = Constants_1.T_BOOLEAN;
}
else {
typeHint = Reflect.getPrototypeOf(value)?.constructor;
if (typeHint && typeHint.name) {
if (typeHint.name === Object.name) {
typeHint = Constants_1.T_OBJECT;
}
else if (typeHint.name === Array.name) {
typeHint = Constants_1.T_ARRAY;
}
}
else {
typeHint = this.getDefaultTypeHint();
}
}
}
}
}
let target = null;
if (typeHint) {
if ((0, lodash_1.isString)(typeHint)) {
propMeta.type = typeHint;
if (propMeta.type === Constants_1.T_ARRAY) {
(0, functions_1.setDefaultArray)(propMeta);
}
}
else if ((0, lodash_1.isFunction)(typeHint)) {
if (typeHint === Date) {
// propMeta.type = 'date' as any;
propMeta.type = Constants_1.T_STRING;
propMeta.format = 'date-time';
}
else {
const name = base_1.ClassUtils.getClassName(typeHint);
if (name === '' || name === Function.name) {
// Function passing the parameter type
// propMeta.$target = typeHint();
target = typeHint();
}
else {
// propMeta.$target = typeHint as Function;
target = typeHint;
}
// maybe follow the object here
if (target.name === Array.name) {
(0, functions_1.setDefaultArray)(propMeta);
}
else {
propMeta.type = Constants_1.T_OBJECT;
}
}
}
}
if (!propMeta.type || ((0, lodash_1.isString)(propMeta.type) && (0, lodash_1.isEmpty)(propMeta.type))) {
if (reflectMetadataType) {
const className = base_1.ClassUtils.getClassName(reflectMetadataType);
if (JsonSchema7_1.JSON_SCHEMA_7_TYPES.includes(className.toLowerCase())) {
propMeta.type = className.toLowerCase();
}
else if (className === Array.name) {
(0, functions_1.setDefaultArray)(propMeta);
}
else {
propMeta.type = Constants_1.T_OBJECT;
target = reflectMetadataType;
}
}
else {
// not reflection data
propMeta.type = this.getDefaultTypeHint();
}
}
if (target) {
if (this.isAppendTargetSet()) {
propMeta.$target = target;
}
const className = base_1.ClassUtils.getClassName(target);
let ref = '#/definitions/' + className;
if (propMeta.type === Constants_1.T_ARRAY) {
propMeta.items = {
$ref: ref
};
}
else {
propMeta['$ref'] = ref;
delete propMeta['type'];
}
if (this.data && !this.data.definitions[className]) {
this.describeClass(target);
}
}
if (!((0, lodash_1.isUndefined)(value) || (0, lodash_1.isNull)(value))) {
propMeta.default = SchemaUtils_1.SchemaUtils.normValue(value);
}
this.propertyPostproces(propMeta);
const data = MetadataRegistry_1.MetadataRegistry.$().find(Constants_1.METATYPE_PROPERTY, (x) => x.target === clazz && x.propertyName === propertyName);
this.appendAdditionalOptions(propMeta, data);
return propMeta;
}
appendAdditionalOptions(propMeta, data) {
if (data && (0, lodash_1.keys)(data).length > 0) {
const metaType = data.metaType;
if (metaType === Constants_1.METATYPE_PROPERTY) {
if ((0, IClassRef_1.isClassRef)(data.type) || (0, IEntityRef_1.isEntityRef)(data.type)) {
// if reference ignore adding
if ((0, lodash_1.get)(this.options, 'deleteReferenceKeys', true)) {
return;
}
}
}
const _keys = (0, lodash_1.keys)(data);
for (const k of _keys) {
if (this.options.keysToSkip.includes(k)) {
continue;
}
if (!propMeta[k] || (0, lodash_1.get)(this.options, 'allowKeyOverride', false)) {
propMeta[k] = data[k];
}
}
}
}
/**
* General handle to check allow options method
*
* @param entry
*/
allowed(entry, klass) {
if (this.options && this.options.allowedProperty && (0, lodash_1.isFunction)(this.options.allowedProperty)) {
return this.options.allowedProperty(entry, klass);
}
return true;
}
describePropertyForRef(klass, property) {
const propMeta = {};
if (property.isCollection()) {
(0, functions_1.setDefaultArray)(propMeta);
if (property.isReference()) {
this.describeTargetRef(property, propMeta, 'collection');
}
else {
const normedType = this.getNormedType(property);
propMeta.items = {
type: normedType
};
this.propertyPostproces(propMeta.items);
}
}
else {
propMeta.type = Constants_1.T_OBJECT;
if (property.isReference()) {
this.describeTargetRef(property, propMeta, 'single');
}
else {
const normedType = this.getNormedType(property);
propMeta.type = normedType;
this.propertyPostproces(propMeta);
}
}
const data = property.getOptions();
this.appendAdditionalOptions(propMeta, data);
// pass data pattern property for later correct selection
if (data[Constants_1.K_PATTERN_PROPERTY]) {
propMeta[Constants_1.K_PATTERN_PROPERTY] = true;
}
if (this.options.postProcess) {
this.options.postProcess(property, propMeta, this);
}
return propMeta;
}
describeTargetRef(property, propMeta, mode) {
let targetRef = property.getTargetRef();
if ((0, lodash_1.get)(this.options, 'deleteReferenceKeys', true)) {
(0, lodash_1.keys)(propMeta).map(k => delete propMeta[k]);
}
else {
this.options.keysToSkip.map(x => delete propMeta[x]);
}
if (targetRef.hasEntityRef()) {
targetRef = targetRef.getEntityRef();
if (mode === 'collection') {
propMeta.type = Constants_1.T_ARRAY;
propMeta.items = { $ref: '#/definitions/' + targetRef.name };
}
else {
propMeta.$ref = '#/definitions/' + targetRef.name;
}
if (this.data && !this.data.definitions[targetRef.name]) {
this.describeEntityRef(targetRef);
}
}
else {
if (mode === 'collection') {
propMeta.type = Constants_1.T_ARRAY;
propMeta.items = { $ref: '#/definitions/' + targetRef.name };
}
else {
propMeta.$ref = '#/definitions/' + targetRef.name;
}
if (this.data && !this.data.definitions[targetRef.name]) {
this.describeClassRef(targetRef);
}
}
}
propertyPostproces(opt) {
if (opt.type === 'date') {
opt.type = Constants_1.T_STRING;
opt.format = 'date-time';
}
}
getNormedType(property) {
let retType = null;
const type = property.getType();
if (this.options.typeConversion) {
retType = this.options.typeConversion(type, property);
}
if (!retType) {
retType = (0, lodash_1.isFunction)(type) ? type.name.toLowerCase() : type;
}
return retType;
}
getDefaultTypeHint() {
return this.options && this.options.defaultTypeHint ? this.options.defaultTypeHint : Constants_1.T_STRING;
}
}
exports.JsonSchema7Serializer = JsonSchema7Serializer;
//# sourceMappingURL=JsonSchema7Serializer.js.map