@sprucelabs/schema
Version:
Static and dynamic binding plus runtime validation and transformation to ensure your app is sound. 🤓
334 lines (333 loc) • 14 kB
JavaScript
import AbstractEntity from '../AbstractEntity.js';
import SpruceError from '../errors/SpruceError.js';
import SchemaRegistry from '../singletons/SchemaRegistry.js';
import { TemplateRenderAs, } from '../types/template.types.js';
import isIdWithVersion from '../utilities/isIdWithVersion.js';
import normalizeSchemaToIdWithVersion from '../utilities/normalizeSchemaToIdWithVersion.js';
import validateSchema from '../utilities/validateSchema.js';
import AbstractField from './AbstractField.js';
class SchemaField extends AbstractField {
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 (isIdWithVersion(item)) {
return item;
}
try {
validateSchema(item);
return item;
}
catch (err) {
throw new SpruceError({
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) => normalizeSchemaToIdWithVersion(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 === TemplateRenderAs.Value) {
valueType = `${nameCamel}Schema${schema.version ? `_${schema.version}` : ''}`;
}
else {
valueType = `${globalNamespace}.${namespace}${version ? `.${version}` : ''}${renderAs === TemplateRenderAs.Type
? `.${namePascal + typeSuffix}`
: `.${namePascal}Schema`}`;
if (renderAs === TemplateRenderAs.Type &&
idsWithVersion.length > 1) {
valueType = `{ id: '${id}', values: ${valueType} }`;
}
}
unions.push({
schemaId: id,
valueType,
});
});
let valueType;
if (renderAs === 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 === TemplateRenderAs.Type) ||
(unions.length > 1 && renderAs === 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({
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 !== null && namespace !== void 0 ? namespace : '**missing**'}'.`,
});
}
let matchedTemplateItem;
matchedTemplateItem = allMatches.find((d) => d.schema.version === version);
if (!matchedTemplateItem) {
throw new SpruceError({
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.getInstance().getSchema(schemaOrId)
: schemaOrId;
validateSchema(schema);
return schema;
});
return schemas;
}
validate(value, options) {
var _a;
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) {
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: `${(_a = this.label) !== null && _a !== void 0 ? _a : 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.js').default
: require('../StaticSchemaEntityImpl.js').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) {
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({
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({
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.';
export default SchemaField;
function throwInvalidReferenceError(item) {
throw new SpruceError({
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)}`,
});
}