UNPKG

@alphanova/builder

Version:

A fully fledged facade that facilitates object manipulation

379 lines (378 loc) 15.6 kB
import { set } from 'lodash'; export class Builder { constructor() { Object.defineProperty(this, "_id", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "object", { enumerable: true, configurable: true, writable: true, value: void 0 }); } createLayout(optional) { return { ...optional, createdAt: { type: 'string', optional: true, default: new Date().toISOString() }, updatedAt: { type: 'string', optional: true, default: new Date().toISOString() } }; } hookId(id) { this._id = id || 'FACTORY'; return this; } hookObject(object) { this.object = (object || this.generate()); return this; } build(object) { return this.instantiate(object[0], object[1]); } create(object, id) { return this.instantiate(id, object); } mutate(new_model) { return this.instantiate(this.id, { ...this.object, ...new_model }); } prune(key) { const obj = { ...this.object }; delete obj[key]; return this.instantiate(this.id, { ...obj }); } update(new_model, updater) { const old = { ...this.object }; const object = { ...(old || { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }), ...new_model, }; this.object = { ...object }; if (updater) { updater(old, this); } return this; } list(object) { return Object.entries(object).reduce((result, [key, value]) => { result.set(key, this.create(value, key)); return result; }, new Map()); } generate() { return Object.fromEntries(Object.entries(this.layout).map(([key, val]) => { const value = val; return [key, value.default]; })); // return JSON.parse(JSON.stringify({ // ...this.inst, createdAt: new Date().toISOString(), // updatedAt: new Date().toISOString() // })) as TModel } handleMissing(value, silent, key) { switch (value.onMissing) { case 'error': default: throw new Error(`Missing attribute: '${key.join(' > ')}'`); case 'warn': if (!silent) console.warn(`Missing attribute: '${key.join(' > ')}'`); break; case 'ignore': break; } } handleMismatch(value, silent, key, data) { switch (value.onMismatch) { case 'warn': if (!silent) console.warn(`Mismatched attribute type: '${typeof data}' for '${key.join(' > ')}: ${data}'. Expected type: ${value.type}`); break; case 'ignore': break; case 'error': default: throw new Error(`Mismatched attribute type: '${typeof data}' for '${key.join(' > ')}: ${data}'. Expected type: ${value.type}`); } } processKeyValidate(key, data, value, updating, silent) { if (!updating) { if (data === undefined) { if (!value.optional) { this.handleMissing(value, !!silent, key); } } if (data === null) { if (value.nullState === 'undefined') { this.handleMissing(value, !!silent, key); } } } if (data !== undefined && data !== null) { switch (value.type) { case 'string': if (typeof data !== 'string') { this.handleMismatch(value, !!silent, key, data); break; } else if (value.min_length && data.length < value.min_length) { throw new Error(`String length too short: '${key.toString()}'`); } else if (value.max_length && data.length > value.max_length) { throw new Error(`String length too long: '${key.toString()}'`); } break; case 'number': if (typeof data !== 'number') { if (typeof data === 'string') { switch (value.onString) { case 'warn': if (!silent) console.warn(`Mismatched attribute type: '${typeof data}' for '${key.toString()}'. Expected type: ${value.type}`); break; case 'error': throw new Error(`Mismatched attribute type: '${typeof data}' for '${key.toString()}'. Expected type: ${value.type}`); case 'ignore': break; case 'convert': return { key, value: Number(data) }; case 'round': return { key, value: Math.round(Number(data)) }; case 'floor': return { key, value: Math.floor(Number(data)) }; case 'ceil': return { key, value: Math.ceil(Number(data)) }; default: this.handleMismatch(value, !!silent, key, data); } } else { this.handleMismatch(value, !!silent, key, data); break; } } if (value.min && data < value.min) { throw new Error(`Number too small: '${key.toString()}'`); } else if (value.max && data > value.max) { throw new Error(`Number too large: '${key.toString()}'`); } break; case 'date': if (typeof data === 'string') { if (value.min && (new Date(data)) < value.min) { throw new Error(`Date too early: '${key.toString()}'`); } else if (value.max && (new Date(data)) > value.max) { throw new Error(`Date too late: '${key.toString()}'`); } return { key, value: new Date(data) }; } else { this.handleMismatch(value, !!silent, key, data); } break; case 'boolean': if (typeof data !== 'boolean') { this.handleMismatch(value, !!silent, key, data); } break; case 'array': { if (!Array.isArray(data)) { this.handleMismatch(value, !!silent, key, data); } else if (value.min_length && data.length < value.min_length) { throw new Error(`Array length too short: '${key.toString()}'`); } else if (value.max_length && data.length > value.max_length) { throw new Error(`Array length too long: '${key.toString()}'`); } const innerLayout = value.elements; if (Array.isArray(data) && innerLayout) { let index = 0; for (const item of data) { const result = this.processKeyValidate([...key, index++], item, innerLayout, updating, silent); if (result) return result; } } break; } case 'object': if (typeof data !== 'object' || Array.isArray(data)) { this.handleMismatch(value, !!silent, key, data); } if (typeof data === 'object' && value.schema) { Object.entries(value.schema).forEach(([k, val]) => { const d = data[k]; const result = this.processKeyValidate([...key, k], d, val, updating, silent); this.handleExtra(data, value.schema, silent, value.stripId); return result; }); } break; } } } handleExtra(obj, layout, silent, stripId) { const allowed_attributes = layout ? Object.keys(layout) : []; const all_attributes = obj ? Object.keys(obj) : []; const extra_attributes = all_attributes.filter((key) => !allowed_attributes.includes(key)); if (extra_attributes.filter(e => e !== 'id').length > 0) { if (!silent) console.warn('Extra attributes found:', extra_attributes.filter(e => e !== 'id')); } extra_attributes.filter(e => stripId ? true : e !== 'id').forEach(key => delete obj[key]); } processValidation(obj, updating, silent) { const newObj = { ...obj }; Object.entries(this.layout).forEach(([key, val]) => { const data = newObj[key]; const result = this.processKeyValidate([key], data, val, updating, silent); if (result) set(newObj, key, result); }); this.handleExtra(newObj, this.layout, silent); } validateObject(object, updating, silent) { const obj = { ...object }; this.processValidation(obj, updating, silent); return obj; } validate(updating, silent) { const obj = { ...this.object }; this.processValidation(obj, updating, silent); return this.mutate(obj); // const object = { ...this.object } as Required<TModel> // const allowed_extra = ['_id', 'id', '__v', 'password'] // const interfaceObj = this.generate() // const objKeys = Object.keys(object || this.object || {}) as Array<keyof TModel> // const interfaceKeys = Object.keys(interfaceObj || {}) as Array<keyof TModel> // // Check if any extra attributes are present in the object // const extraKeys = objKeys.filter((key) => !interfaceKeys.includes(key) && !allowed_extra.includes(key.toString())) // if (extraKeys.length > 0) { // if (!silent) // console.warn('Extra attributes found:', extraKeys) // extraKeys.forEach(key => delete object[key]) // } // allowed_extra.forEach(key => delete object[key as keyof TModel]) // // Check if any required attributes are missing from the object // const missingKeys = interfaceKeys.filter((key) => { // if (objKeys.includes(key)) { // return false // Skip keys that exist in the object // } // if (this.layout?.includes(key)) { // return false // } // return true // }) // Object.entries(object).forEach(([key, value]) => { // if (!this.layout.includes(key as TOpt) && (typeof value === 'undefined' || value === null)) { // delete object[key as keyof TModel] // missingKeys.push(key as keyof TModel) // } // }) // if (missingKeys.length > 0) { // if (!silent) { // console.error('Missing attributes:', missingKeys) // } // if (!updating) { // return undefined // } // } // // Check if any attribute types do not match // for (const key of interfaceKeys) { // if (typeof (object[key]) !== 'undefined' && (object[key] !== null) && typeof (object[key]) !== typeof interfaceObj[key]) { // if (!silent) // console.error(`Mismatched attribute type: '${typeof (object[key])}' for '${String(key)}'. Expected type: ${typeof interfaceObj[key]}`) // return undefined // } // } // return this as unknown as TBuilder } fixateExtra(obj) { // This method is not type safe. Make sure to validate the object after calling this method const fixed = { ...obj }; const fix = []; this.foreign?.forEach((foreign) => { const data = fixed[foreign.key]; if (foreign.isArray) { if (data && typeof data === 'object') { fix.push(...data.map((item) => ({ data: item, type: foreign.model }))); } else { console.error('data is incorrectly shaped: ', data); } fixed[foreign.key] = data?.map((item) => item.id); } else if (data && typeof data === 'object' && '_id' in data) { fix.push({ data: data, type: foreign.model }); fixed[foreign.key] = data._id; } }); return { fixed, fix }; } extractExtra(obj) { // This method should be type safe. Make sure to validate the object after calling this method const fix = { ...obj }; const extra = []; this.many?.forEach((foreign) => { const data = fix[foreign.key]; if (data) { extra.push({ data: data, type: foreign.model }); if (foreign.reference) { fix[foreign.key] = data?.map((item) => item.id); } else { delete fix[foreign.key]; } } }); return { extracted: fix, extra }; } get extra() { return this.foreign?.map((foreign) => foreign.key.toString()); } get safeExtract() { if (!this.object) throw new Error('Object not provided'); return this.object; } get generateDefault() { return this.generate(); } get id() { if (this._id) return this._id; else throw new Error('Id not provided'); } get getObj() { return this.object; } toString() { return JSON.stringify(this.object, null, 2); } }