@alphanova/builder
Version:
A fully fledged facade that facilitates object manipulation
379 lines (378 loc) • 15.6 kB
JavaScript
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);
}
}