@sprucelabs/schema
Version:
Static and dynamic binding plus runtime validation and transformation to ensure your app is sound. 🤓
338 lines (337 loc) • 14.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const AbstractEntity_1 = __importDefault(require("../AbstractEntity"));
const SpruceError_1 = __importDefault(require("../errors/SpruceError"));
const SchemaRegistry_1 = __importDefault(require("../singletons/SchemaRegistry"));
const template_types_1 = require("../types/template.types");
const isIdWithVersion_1 = __importDefault(require("../utilities/isIdWithVersion"));
const normalizeSchemaToIdWithVersion_1 = __importDefault(require("../utilities/normalizeSchemaToIdWithVersion"));
const validateSchema_1 = __importDefault(require("../utilities/validateSchema"));
const AbstractField_1 = __importDefault(require("./AbstractField"));
class SchemaField extends AbstractField_1.default {
static mapFieldDefinitionToSchemasOrIdsWithVersion(field) {
const { options } = field;
const schemasOrIds = [
...(options.schema ? [options.schema] : []),
...(options.schemaId ? [options.schemaId] : []),
...(options.schemas || []),
...(options.schemaIds || []),
...(options.schemasCallback ? options.schemasCallback() : []),
];
return schemasOrIds.map((item) => {
if (typeof item === 'string') {
return { id: item };
}
if ((0, isIdWithVersion_1.default)(item)) {
return item;
}
try {
(0, validateSchema_1.default)(item);
return item;
}
catch (err) {
throw new SpruceError_1.default({
code: 'INVALID_SCHEMA',
schemaId: JSON.stringify(options),
originalError: err,
errors: ['invalid_schema_field_options'],
});
}
});
}
static mapFieldDefinitionToSchemaIdsWithVersion(field) {
const schemasOrIds = this.mapFieldDefinitionToSchemasOrIdsWithVersion(field);
const ids = schemasOrIds.map((item) => (0, normalizeSchemaToIdWithVersion_1.default)(item));
return ids;
}
static generateTypeDetails() {
return {
valueTypeMapper: 'SchemaFieldValueTypeMapper<F extends SchemaFieldFieldDefinition? F : SchemaFieldFieldDefinition, CreateEntityInstances>',
};
}
static generateTemplateDetails(options) {
const { templateItems, renderAs, definition, globalNamespace, language, } = options;
const { isArray } = definition;
const { typeSuffix = '' } = definition.options;
const idsWithVersion = this.mapFieldDefinitionToSchemaIdsWithVersion(definition);
const unions = [];
idsWithVersion.forEach((idWithVersion) => {
const { version } = idWithVersion;
const { namePascal, namespace, id, nameCamel, schema } = this.findSchemaInTemplateItems(idWithVersion, templateItems);
let valueType;
if (language === 'go') {
valueType = `${namePascal}`;
}
else if (renderAs === template_types_1.TemplateRenderAs.Value) {
valueType = `${nameCamel}Schema${schema.version ? `_${schema.version}` : ''}`;
}
else {
valueType = `${globalNamespace}.${namespace}${version ? `.${version}` : ''}${renderAs === template_types_1.TemplateRenderAs.Type
? `.${namePascal + typeSuffix}`
: `.${namePascal}Schema`}`;
if (renderAs === template_types_1.TemplateRenderAs.Type &&
idsWithVersion.length > 1) {
valueType = `{ id: '${id}', values: ${valueType} }`;
}
}
unions.push({
schemaId: id,
valueType,
});
});
let valueType;
if (renderAs === template_types_1.TemplateRenderAs.Value) {
valueType =
unions.length === 1
? unions[0].valueType
: '[' +
unions.map((item) => item.valueType).join(', ') +
']';
}
else {
valueType = unions.map((item) => item.valueType).join(' | ');
const shouldRenderAsArray = (isArray && renderAs === template_types_1.TemplateRenderAs.Type) ||
(unions.length > 1 && renderAs === template_types_1.TemplateRenderAs.SchemaType);
const arrayNotation = shouldRenderAsArray ? '[]' : '';
if (language === 'go') {
valueType = `*${arrayNotation}${valueType}`;
}
else {
valueType = `${shouldRenderAsArray && unions.length > 1
? `(${valueType})`
: `${valueType}`}${arrayNotation}`;
}
}
return {
valueType,
};
}
static findSchemaInTemplateItems(idWithVersion, templateItems) {
const { id, namespace, version } = idWithVersion;
let allMatches = templateItems.filter((item) => {
if (!item.id) {
throwInvalidReferenceError(item);
}
return item.id.toLowerCase() === id.toLowerCase();
});
if (namespace) {
allMatches = allMatches.filter((item) => {
if (!item.namespace) {
throwInvalidReferenceError(item);
}
return item.namespace.toLowerCase() === namespace.toLowerCase();
});
}
if (allMatches.length === 0) {
throw new SpruceError_1.default({
code: 'SCHEMA_NOT_FOUND',
schemaId: id,
friendlyMessage: `Template generation failed. I could not find a schema that was being referenced. I was looking for a schema with the id of '${id}' and namespace '${namespace ?? '**missing**'}'.`,
});
}
let matchedTemplateItem;
matchedTemplateItem = allMatches.find((d) => d.schema.version === version);
if (!matchedTemplateItem) {
throw new SpruceError_1.default({
code: 'VERSION_NOT_FOUND',
schemaId: id,
});
}
return matchedTemplateItem;
}
static mapFieldDefinitionToSchemas(definition, options) {
const { schemasById: schemasById = {} } = options || {};
const schemasOrIds = SchemaField.mapFieldDefinitionToSchemasOrIdsWithVersion(definition);
const schemas = schemasOrIds.map((schemaOrId) => {
const schema = typeof schemaOrId === 'string'
? schemasById[schemaOrId] ||
SchemaRegistry_1.default.getInstance().getSchema(schemaOrId)
: schemaOrId;
(0, validateSchema_1.default)(schema);
return schema;
});
return schemas;
}
validate(value, options) {
const errors = super.validate(value, options);
// do not validate schemas by default, very heavy and only needed when explicitly asked to
if (value instanceof AbstractEntity_1.default) {
try {
value.validate();
return [];
}
catch (err) {
errors.push({
originalError: err,
errors: err.options.errors,
code: 'INVALID_PARAMETER',
name: this.name,
});
}
}
if (errors.length === 0 && value) {
if (typeof value !== 'object') {
errors.push({
code: 'INVALID_PARAMETER',
name: this.name,
friendlyMessage: `${this.label ?? this.name} must be an object!`,
});
}
else {
let schemas;
try {
// pull schemas out of our own definition
schemas = SchemaField.mapFieldDefinitionToSchemas(this.definition, options);
}
catch (err) {
errors.push({
code: 'INVALID_PARAMETER',
name: this.name,
originalError: err,
friendlyMessage: err.message,
});
}
if (schemas && schemas.length === 0) {
errors.push({ code: 'MISSING_PARAMETER', name: this.name });
}
// if we are validating schemas, we look them all up by id
let instance;
if (schemas && schemas.length === 1) {
// @ts-ignore warns about infinite recursion, which is true, because relationships between schemas can go forever
instance = this.Schema(schemas[0], value);
}
else if (schemas && schemas.length > 0) {
const { id, version, values } = value || {};
if (!values) {
errors.push({
name: this.name,
label: this.label,
code: 'INVALID_PARAMETER',
friendlyMessage: 'You need to add `values` to the value of ' +
this.name,
});
}
else if (!id) {
errors.push({
name: this.name,
label: this.label,
code: 'INVALID_PARAMETER',
friendlyMessage: 'You need to add `id` to the value of ' +
this.name,
});
}
else {
const matches = schemas.filter((schema) => schema.id === id &&
(!version || schema.version === version));
if (matches.length !== 1) {
errors.push({
name: this.name,
label: this.label,
code: 'INVALID_PARAMETER',
friendlyMessage: `Could not find a schema by id '${id}'${version
? ` and version '${version}'`
: ' with no version. Try adding a version to disambiguate.'}.`,
});
}
else {
instance = this.Schema(matches[0], values);
}
}
}
if (instance) {
try {
instance.validate();
}
catch (err) {
errors.push({
code: 'INVALID_PARAMETER',
errors: err.options.errors,
name: this.name,
originalError: err,
label: this.label,
});
}
}
}
}
return errors;
}
Schema(schema, value) {
const Class = schema.dynamicFieldSignature
? require('../DynamicSchemaEntityImplementation').default
: require('../StaticSchemaEntityImpl').default;
return new Class(schema, value);
}
toValueType(value, options) {
const { createEntityInstances, schemasById: schemasById = {} } = options || {};
// try and pull the schema definition from the options and by id
const destinationSchemas = SchemaField.mapFieldDefinitionToSchemas(this.definition, {
schemasById,
});
const isUnion = destinationSchemas.length > 1;
let instance;
if (value instanceof AbstractEntity_1.default) {
instance = value;
}
// if we are only pointing 1 one possible definition, then mapping is pretty easy
else if (!isUnion) {
instance = this.Schema(destinationSchemas[0], value);
}
else {
// this could be one of a few types, lets check the "schemaId" prop
const { id, values } = value;
const allMatches = destinationSchemas.filter((def) => def.id === id);
let matchedSchema;
if (allMatches.length === 0) {
throw new SpruceError_1.default({
code: 'TRANSFORMATION_ERROR',
fieldType: 'schema',
name: this.name,
incomingValue: value,
incomingTypeof: typeof value,
});
}
if (allMatches.length > 1) {
if (value.version) {
matchedSchema = allMatches.find((def) => def.version === value.version);
}
if (!matchedSchema) {
throw new SpruceError_1.default({
code: 'VERSION_NOT_FOUND',
schemaId: id,
});
}
}
else {
matchedSchema = allMatches[0];
}
instance = this.Schema(matchedSchema, values);
}
if (createEntityInstances) {
return instance;
}
const getValueOptions = {
validate: false,
...options,
fields: undefined,
};
if (isUnion) {
return {
id: instance.schemaId,
values: instance.getValues(getValueOptions),
};
}
return instance.getValues(getValueOptions);
}
}
SchemaField.description = 'A way to map relationships.';
exports.default = SchemaField;
function throwInvalidReferenceError(item) {
throw new SpruceError_1.default({
code: 'INVALID_SCHEMA_REFERENCE',
friendlyMessage: `Generating template failed because one of your schema references (the schema a field with 'type=schema' points to) is missing a namespace. Look through your builders for a field pointing to something like:\n\n${JSON.stringify(item, null, 2)}`,
});
}