UNPKG

@sprucelabs/schema

Version:

Static and dynamic binding plus runtime validation and transformation to ensure your app is sound. 🤓

334 lines (333 loc) • 14 kB
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)}`, }); }