UNPKG

@martinmilo/verve

Version:

TypeScript domain modeling library with field-level authorization, business rule validation, and context-aware access control

179 lines 8.15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_OBJECT_PROPS = void 0; exports.createModelClass = createModelClass; const context_1 = require("../../context"); const authorization_1 = require("../../authorization"); const FieldInitializer_1 = require("../initializers/FieldInitializer"); const ValidationInitializer_1 = require("./ValidationInitializer"); const utils_1 = require("../core/utils"); const ValueInitializer_1 = require("./ValueInitializer"); const errors_1 = require("../../errors"); const Model_1 = require("../core/Model"); const constants_1 = require("../../constants"); exports.DEFAULT_OBJECT_PROPS = { writable: false, enumerable: false, configurable: false }; function createModelClass() { return class extends (0, context_1.WithContext)((0, authorization_1.WithAuthorization)((Model_1.Model))) { constructor(internal) { if (internal !== constants_1.MODEL_CONSTRUCTOR) { throw new errors_1.VerveError(errors_1.ErrorCode.DIRECT_INSTANTIATION_NOT_ALLOWED); } super(); } static make(data) { const instance = new this(constants_1.MODEL_CONSTRUCTOR); instance[constants_1.MODEL_INITIALIZER]('make'); // 1. Initialize field instances on the model FieldInitializer_1.FieldInitializer.initialize(instance, this.schema); // 2. Initialize field values on the model (i.e. save on model state and as object property) ValueInitializer_1.ValueInitializer.initialize(instance, data, { isHydrating: false }); // 3. Create fields proxy const proxy = createFieldsProxy(instance, this.schema); instance[constants_1.MODEL_PROXY](proxy); // 4. Validate field values ValidationInitializer_1.ValidationInitializer.initialize(instance, this.schema); // 5. Record initial changes for (const key in instance[constants_1.MODEL_STATE]()) { instance[constants_1.MODEL_CHANGE_LOG]({ field: key, currentValue: instance[constants_1.MODEL_STATE]()[key], previousValue: undefined, timestamp: new Date(), }); } return proxy; } static from(data) { const instance = new this(constants_1.MODEL_CONSTRUCTOR); instance[constants_1.MODEL_INITIALIZER]('from'); // 1. Initialize field instances on the model FieldInitializer_1.FieldInitializer.initialize(instance, this.schema); // 2. Initialize field values on the model (i.e. save on model state and as object property) ValueInitializer_1.ValueInitializer.initialize(instance, data, { isHydrating: true }); // 3. Create fields proxy const proxy = createFieldsProxy(instance, this.schema); instance[constants_1.MODEL_PROXY](proxy); // 4. Validate field values ValidationInitializer_1.ValidationInitializer.initialize(instance, this.schema); // 5. Set initial state (we need to make a copy of the state to avoid mutating the original state) instance[constants_1.MODEL_INITIAL_STATE]({ ...instance[constants_1.MODEL_STATE]() }); return proxy; } }; } function createFieldsProxy(instance, schema) { const fields = instance[constants_1.MODEL_FIELDS](); const baseModelMethods = (0, utils_1.getBaseModelMethods)(Model_1.Model); const recordChange = (fieldInstance, newValue) => { fieldInstance.set(newValue); }; return new Proxy(instance, { get(target, key, receiver) { if (Object.prototype.hasOwnProperty.call(schema, key)) { const field = fields[key]; if (field.options.compute) { return field.compute(); } const fieldValue = field.get(); // Wrap objects and arrays in nested proxies to track and log state changes if (isArrayOrObject(fieldValue)) { return createNestedProxy({ key, value: fieldValue, instance: field, recordChange: recordChange.bind(null, field) }); } return fieldValue; } const value = Reflect.get(target, key, receiver); // We need to bind the method to the original target to preserve private field access // But we only do so for the methods of the base model class, since these access private fields // Example: user.isNew() should return the original target's isNew method if (typeof value === 'function' && baseModelMethods.has(key)) { return value.bind(target); } return value; }, set(target, key, value, receiver) { if (Object.prototype.hasOwnProperty.call(schema, key)) { const field = fields[key]; if (field.options.compute) { throw new errors_1.VerveError(errors_1.ErrorCode.FIELD_IS_COMPUTED, { field: key, model: field.metadata.model }); } if (!field.isWritable()) { throw new errors_1.VerveError(errors_1.ErrorCode.FIELD_NOT_WRITABLE, { field: key, model: field.metadata.model }); } recordChange(field, value); return true; } return Reflect.set(target, key, value, receiver); }, }); } function createNestedProxy(field) { if (!isArrayOrObject(field.value)) { return field.value; } return Array.isArray(field.value) ? createArrayProxy(field) : createObjectProxy(field); } function createArrayProxy(field) { return new Proxy(field.value, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); // For array methods that might mutate, wrap them to record changes AFTER they execute if (typeof value === 'function' && typeof prop === 'string') { const originalMethod = value; return function (...args) { const result = originalMethod.apply(target, args); // Only record change if the method actually mutated the array // We can check this by seeing if the method is a known mutating method const mutatingMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin']; if (mutatingMethods.includes(prop)) { field.recordChange(target); } return result; }; } if (isArrayOrObject(value)) { return createNestedProxy(field); } return value; }, set(target, prop, value, receiver) { const result = Reflect.set(target, prop, value, receiver); if (result) { field.recordChange(target); } return result; } }); } function createObjectProxy(field) { return new Proxy(field.value, { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); if (isArrayOrObject(value)) { return createNestedProxy(field); } return value; }, set(target, prop, value, receiver) { const result = Reflect.set(target, prop, value, receiver); if (result) { field.recordChange(target); } return result; } }); } function isArrayOrObject(value) { if (value instanceof Model_1.Model) { return false; } if (value instanceof Date) { return false; } return typeof value === 'object' && value !== null; } //# sourceMappingURL=ModelInitializer.js.map