@martinmilo/verve
Version:
TypeScript domain modeling library with field-level authorization, business rule validation, and context-aware access control
179 lines • 8.15 kB
JavaScript
;
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