UNPKG

fieldify

Version:

Fieldify object manipulation & validator

421 lines (363 loc) 15.1 kB
// The code defines a JavaScript module that exports a class called "fieldifySchema". // The class is responsible for compiling and processing a schema object. // Define a regular expression to match keys starting with "$". const currentSet = 'F2022V1' const leafRegex = /^\$/ // The main class "fieldifySchema". class fieldifySchema { constructor(context) { this.context = context this.routing = {} } static extractControllers(schema) { const ret = { params: {}, fields: [], array: {}, isNested: false } for (var key in schema) { const value = schema[key] if (leafRegex.test(key)) { ret.params[key] = value } else if (Array.isArray(value)) { ret.array[key] = value ret.isNested = true } else { ret.fields.push(key) ret.isNested = true } } return (ret) } static schemaIterator(data, onField, line = [], merge) { const ctrl = { ...fieldifySchema.extractControllers(data), ...merge } onField([...line], ctrl) for (var field of ctrl.fields) { line.push(field) fieldifySchema.schemaIterator(data[field], onField, line, { isArray: false }) } for (var field in ctrl.array) { const value = ctrl.array[field] line.push(field) fieldifySchema.schemaIterator(value[0], onField, line, { isArray: true }) } line.pop() } // Compiles a schema object and returns the compiled result. compile(schema) { const ret = { errors: [], warnings: [] } // TODO: process field expension // Iterate over the schema using the schemaIterator function. fieldifySchema.schemaIterator(schema, (line, ctrl) => { const key = line.join(".") this.routing[key] = ctrl // Resolve field type only on data field if (ctrl.isNested === false) { const name = ctrl.params.$type if (!name) { ret.errors.push({ field: key, message: `Define a type for this field'` }) return } const type = this.context.getType(name) if (!type) { ret.errors.push({ field: key, message: `Can not find type '${ctrl.params.$type}'` }) return } ctrl.type = type // TODO: sanitize type options } }) return (ret) } // Returns the routing information for a given key. route(key) { return (this.routing[key]) } // Converts data to a string representation based on the schema. async encode(data, user) { return (this.input(data, { user, rejectUnknown: false, rejectCast: false, rejectRequired: false, onAnalysis: async (data) => data.ctrl.type.encode(data) })) } // Decodes a string representation and returns the decoded data based on the schema. async strictDecode(data, user) { return (this.input(data, { onAnalysis: async (data) => data.ctrl.type.decode(data) })) } // Decodes a string representation and returns the decoded data based on the schema. async decode(data, user) { return (this.input(data, { user, rejectUnknown: false, rejectCast: false, rejectRequired: false, onAnalysis: async (data) => data.ctrl.type.decode(data) })) } // Performs strict verification on the data based on the schema. async strictVerify(data, user) { return (this.input(data, { user, onAnalysis: async (data) => data.ctrl.type.verify(data) })) } // Performs verification on the data based on the schema. async verify(data, user) { return (this.input(data, { user, rejectUnknown: false, onAnalysis: async (data) => data.ctrl.type.verify(data) })) } // Extracts data from the input without performing validation or casting. async filter(data, user) { return (this.input(data, { user, rejectUnknown: false, rejectCast: false, rejectRequired: false })) } // Processes the input data based on the schema and the provided options. async input(data, options) { const ret = { error: false, fields: {}, result: {} } // Set default options and merge with the provided options. options = { onAnalysis: async (a) => { a.result = a.value }, rejectUnknown: true, rejectCast: true, rejectRequired: true, ...options } // Asynchronously runs the analysis on the data. const runAnalysis = async (data) => { data.error = null data.virtLink = data.virt.join(".") data.realLink = data.real.join(".") // execute general verification // usualy the type controller await options.onAnalysis(data) if (data.drop === true) return (false) // next, the user can defined his own // validation callback if (data.ctrl.params?.$validate) await data.ctrl.params.$validate(data) if (data.error) { ret.error = true ret.fields[data.realLink] = data.error return (false) } // the user can use drop to prevent field // to be assigned else if (data.drop === true) return (false) return (true) } // Recursive iterator function to process the input data. const iterator = async (data, result, virt = [], real = []) => { // Check for required fields const ctrl = this.routing[virt.join(".")] // Search in fields for (var field of ctrl.fields) { const virtFieldKey = [...virt, field].join(".") const realFieldKey = [...real, field].join(".") const fieldAvailable = data.hasOwnProperty(field) const fieldCtrl = this.routing[virtFieldKey] if (fieldCtrl?.params?.$required === true && !fieldAvailable && options.rejectRequired === true) { ret.error = true ret.fields[realFieldKey] = `Field '${realFieldKey}' is required` } } // Search in array for (var field in ctrl.array) { const value = ctrl.array[field] const virtFieldKey = [...virt, field].join(".") const realFieldKey = [...real, field].join(".") const fieldAvailable = data.hasOwnProperty(field) const fieldCtrl = this.routing[virtFieldKey] if (fieldCtrl?.params?.$required === true && !fieldAvailable && options.rejectRequired === true) { ret.error = true ret.fields[realFieldKey] = `Field '${realFieldKey}' is required` } } // Follow input data for (var key in data) { const value = data[key] if (Array.isArray(value)) { virt.push(key) real.push(key) const virtKey = virt.join(".") const realKey = real.join(".") // Check if the controller exists const ctrl = this.routing[virtKey] if (!ctrl) { if (options.rejectUnknown === true) { ret.error = true ret.fields[realKey] = `Unknown array '${virtKey}'` } virt.pop() real.pop() continue } // Check if the schema requires an array if (ctrl.isArray !== true) { if (ctrl.type.noInputCast === true) { const analysis = { context: this.context, virt, real, value, ctrl, options } const ret = await runAnalysis(analysis) if (ret === true) result[key] = analysis.result virt.pop() real.pop() continue } if (options.rejectCast === true) { ret.error = true ret.fields[realKey] = `Can not cast array into '${virtKey}' field` } virt.pop() real.pop() continue } // Check the size of the array based on the provided configuration parameters. // Check if the minimum length parameter exists in the control parameters if ("$min" in ctrl.params) { // Check if the length of the array is less than the minimum if (value.length < ctrl.params.$min) { ret.error = true; ret.fields[realKey] = `The array is too short and must be at least ${ctrl.params.$min} element(s) long`; virt.pop(); real.pop(); continue; } } // Check if the maximum length parameter exists in the control parameters if ("$max" in ctrl.params) { // Check if the length of the array exceeds the maximum if (value.length > ctrl.params.$max) { ret.error = true; ret.fields[realKey] = `The array is too long and must be no more than ${ctrl.params.$max} element(s) long`; virt.pop(); real.pop(); continue; } } // Iterate over the array if (ctrl.isNested === true) { const rvalue = result[key] = [] for (var index = 0; index < value.length; index++) { real.pop() real.push(`${key}[${index}]`) if (value[index] instanceof Object) { const rindex = rvalue.push({}) await iterator(value[index], rvalue[rindex - 1], [...virt], [...real]) } } } else { const rvalue = result[key] = [] for (var index = 0; index < value.length; index++) { real.pop() real.push(`${key}[${index}]`) const analysis = { context: this.context, virt: real, real, value: value[index], ctrl, options } const ret = await runAnalysis(analysis) if (ret === true) rvalue.push(analysis.result) } } virt.pop() real.pop() } else { virt.push(key) real.push(key) const virtKey = virt.join(".") const realKey = real.join(".") // Check if the controller exists const ctrl = this.routing[virtKey] if (!ctrl) { if (options.rejectUnknown === true) { ret.error = true ret.fields[realKey] = `Unknown field '${virtKey}'` } virt.pop() real.pop() continue } // Check if the schema requires an array if (ctrl.isArray === true) { if (options.rejectCast === true) { ret.error = true ret.fields[realKey] = `Can not cast non-array into '${virtKey}' field` } virt.pop() real.pop() continue } // Check if the schema requires an object if (ctrl.isNested === true && !(value instanceof Object)) { if (options.rejectCast === true) { ret.error = true ret.fields[realKey] = `Can not cast non-object into '${virtKey}' field` } virt.pop() real.pop() continue } if (ctrl.isNested === true && value instanceof Object) { result[key] = {} await iterator(value, result[key], [...virt], [...real]) } else { const analysis = { context: this.context, virt, real, value, ctrl, options } const ret = await runAnalysis(analysis) if (ret === true) result[key] = analysis.result } virt.pop() real.pop() } } } await iterator(data, ret.result) if (ret.error === true) ret.result = {} return (ret) } } module.exports = fieldifySchema