@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
JavaScript
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;