UNPKG

@sprucelabs/schema

Version:

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

184 lines (183 loc) • 7.46 kB
import AbstractEntity from './AbstractEntity.js'; import SpruceError from './errors/SpruceError.js'; import FieldFactory from './factories/FieldFactory.js'; import cloneDeepPreservingInstances from './utilities/cloneDeepPreservingInstances.js'; import normalizeFieldValue, { normalizeValueToArray, } from './utilities/normalizeFieldValue.js'; class StaticSchemaEntityImpl extends AbstractEntity { constructor(schema, values) { super(schema); this.values = {}; this.schema = schema; this.fields = {}; this.buildFields(); const v = { ...this.values, ...values, }; this.values = cloneDeepPreservingInstances(v); } buildFields() { const fieldDefinitions = this.schema.fields; if (!fieldDefinitions) { throw new Error(`SchemaEntity requires fields. If you want to use dynamicFieldSignature, try DynamicSchemaEntity.`); } Object.keys(fieldDefinitions).forEach((name) => { const definition = fieldDefinitions[name]; const field = FieldFactory.Field(name, definition); this.fields[name] = field; if (definition.value) { this.set(name, definition.value); } }); } normalizeValue(forField, value, options) { var _a, _b; const field = this.fields[forField]; const overrideOptions = { ...(options !== null && options !== void 0 ? options : {}), ...((_b = (_a = options === null || options === void 0 ? void 0 : options.byField) === null || _a === void 0 ? void 0 : _a[forField]) !== null && _b !== void 0 ? _b : {}), }; return normalizeFieldValue(this.schemaId, this.name, {}, field, value, overrideOptions); } get(fieldName, options = {}) { const value = this.values[fieldName] !== undefined ? this.values[fieldName] : undefined; return this.normalizeValue(fieldName, value, options); } set(fieldName, value, options = {}) { const localValue = this.normalizeValue(fieldName, value, options); this.values[fieldName] = localValue; return this; } isValid(options = {}) { try { this.validate(options); return true; } catch { return false; } } pluckExtraFields(values, schema) { const extraFields = []; if (schema.fields) { const passedFields = Object.keys(values); const expectedFields = Object.keys(schema.fields); passedFields.forEach((passed) => { if (expectedFields.indexOf(passed) === -1) { extraFields.push(passed); } }); } return extraFields; } validate(options = {}) { const errors = []; const extraFields = this.pluckExtraFields(this.values, this.schema); if (extraFields.length > 0) { extraFields.forEach((name) => { errors.push({ name, code: 'UNEXPECTED_PARAMETER', friendlyMessage: `\`${name}\` does not exist.`, }); }); } this.getNamedFields(options).forEach((namedField) => { var _a, _b; const { name, field } = namedField; let valueAsArray = normalizeValueToArray(this.values[name]); if (field.isRequired && field.isArray && (!this.values[name] || valueAsArray.length < ((_a = field.minArrayLength) !== null && _a !== void 0 ? _a : 1))) { errors.push({ code: !this.values[name] ? 'MISSING_PARAMETER' : 'INVALID_PARAMETER', name, friendlyMessage: !this.values[name] ? `${field.label ? `'${field.label}'` : 'This'} is required!` : `${field.label ? `'${field.label}'` : 'You'} must ${field.label ? 'have' : 'select'} at least ${field.minArrayLength} value${field.minArrayLength === 1 ? '' : 's'}. I found ${valueAsArray.length}!`, }); } else { if ((!field.isArray || ((_b = field.minArrayLength) !== null && _b !== void 0 ? _b : 0) > 0) && valueAsArray.length === 0) { valueAsArray = [undefined]; } for (const value of valueAsArray) { const fieldErrors = field.validate(value, { schemasById: {}, }); if (fieldErrors.length > 0) { errors.push(...fieldErrors); } } } }); if (errors.length > 0) { throw new SpruceError({ code: 'VALIDATION_FAILED', schemaId: this.schemaId, schemaName: this.name, errors, }); } } getDefaultValues(options = {}) { const values = {}; this.getNamedFields().forEach((namedField) => { const { name, field } = namedField; if (typeof field.definition.defaultValue !== 'undefined') { // @ts-ignore values[name] = this.normalizeValue(name, field.definition.defaultValue, options); } }); return values; } getValues(options) { const values = {}; let { fields = Object.keys(this.fields), shouldIncludePrivateFields: includePrivateFields = true, shouldIncludeNullAndUndefinedFields = true, excludeFields, } = options || {}; this.getNamedFields().forEach((namedField) => { const { name, field } = namedField; const shouldSkipBecauseNotSet = !shouldIncludeNullAndUndefinedFields && !(name in this.values); const shouldSkipBecauseUndefinedOrNull = !shouldIncludeNullAndUndefinedFields && (this.values[name] === undefined || this.values[name] === null); const shouldSkipBecauseExcluded = excludeFields === null || excludeFields === void 0 ? void 0 : excludeFields.includes(name); if (shouldSkipBecauseNotSet || shouldSkipBecauseUndefinedOrNull || shouldSkipBecauseExcluded) { return; } if (fields.indexOf(name) > -1 && (includePrivateFields || !field.isPrivate)) { const value = this.get(name, options); values[name] = value; } }); return values; } setValues(values) { this.getNamedFields().forEach((namedField) => { const { name } = namedField; const value = values[name]; if (typeof value !== 'undefined') { this.set(name, value); } }); return this; } getNamedFields(options = {}) { const namedFields = []; const { fields = Object.keys(this.fields) } = options; fields.forEach((name) => { const field = this.fields[name]; namedFields.push({ name, field }); }); return namedFields; } } StaticSchemaEntityImpl.enableDuplicateCheckWhenTracking = true; export default StaticSchemaEntityImpl;