UNPKG

@storable/model

Version:

Base class providing typed properties, validation and serialization

497 lines (382 loc) 11.7 kB
"use strict"; exports.__esModule = true; exports.field = field; exports.remoteMethod = remoteMethod; exports.Model = void 0; var _util = require("util"); var _util2 = require("@storable/util"); var _isEmpty = _interopRequireDefault(require("lodash/isEmpty")); var _field = require("./field"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } class Model { constructor(object = {}, { isDeserializing } = {}) { if (typeof object !== 'object') { throw new Error(`Type mismatch (model: '${this.constructor.getName()}', expected: 'object', provided: '${typeof object}')`); } const isNew = isDeserializing ? object._isNew : true; this._fieldValues = {}; this._savedFieldValues = {}; this.constructor.forEachField(field => { const name = isDeserializing ? field.serializedName : field.name; if (Object.prototype.hasOwnProperty.call(object, name)) { const value = object[name]; this._setFieldValue(field, value, { isDeserializing }); return; } if (isNew) { if (field.default !== undefined) { this._applyFieldDefault(field); } else { this._initializeFieldValue(field); } } }); if (isNew) { this.markAsNew(); } } // === Serialization === serialize({ filter, _level = 0 } = {}) { const result = {}; if (this.isNew()) { result._isNew = true; } result._type = this.constructor.getName(); this.constructor.forEachField(field => { if (!this._fieldHasBeenSet(field)) { return; } if (filter && !filter(this, field)) { return; } let value = this._getFieldValue(field); value = field.serializeValue(value, { filter, _level }); result[field.serializedName] = value; }); return result; } toJSON() { return this.serialize(); } static deserialize(object) { return new this(object, { isDeserializing: true }); } // === Core === static defineField(name, type, options, descriptor) { if (descriptor.initializer) { options = _extends({}, options, { default: descriptor.initializer }); } const field = this.setField(name, type, options); descriptor.get = function () { return this._getFieldValue(field); }; descriptor.set = function (val) { return this._setFieldValue(field, val); }; delete descriptor.initializer; delete descriptor.writable; } static getField(name) { var _this$_fields; return (_this$_fields = this._fields) === null || _this$_fields === void 0 ? void 0 : _this$_fields[name]; } static setField(name, type, options) { if (!Object.prototype.hasOwnProperty.call(this, '_fields')) { this._fields = _extends({}, this._fields); } let field = this._fields[name]; if (field) { throw new Error(`Field already exists (name: '${name}')`); } field = new _field.Field(name, type, options); this._fields[name] = field; return field; } static forEachField(func) { if (this._fields) { for (const field of Object.values(this._fields)) { const result = func(field); if (result !== undefined) { // Early return if the function returned something return result; } } } } static canBeSubmodel() { return true; } static fieldValueIsSubmodel(value) { return (value === null || value === void 0 ? void 0 : value.isOfType) && value.isOfType('Model') && value.constructor.canBeSubmodel(); } forEachSubmodel(func) { return this.constructor.forEachField(field => { const value = this._getFieldValue(field); return (0, _util2.findFromOneOrMany)(value, value => { if (this.constructor.fieldValueIsSubmodel(value)) { return func(value) !== undefined; } }); }); } _initializeFieldValue(field) { if (field.isArray) { this._fieldValues[field.name] = []; } } _getFieldValue(field) { return this._fieldValues[field.name]; } _setFieldValue(field, value, { isDeserializing } = {}) { if (!isDeserializing) { this._saveFieldValue(field); } const registry = this.constructor._getRegistry({ throwIfNotFound: false }); value = field.createValue(value, { registry, isDeserializing }); this._fieldValues[field.name] = value; return value; } _applyFieldDefault(field) { let value = field.default; while (typeof value === 'function') { value = value.call(this); } this._setFieldValue(field, value); } _fieldHasBeenSet(field) { return this._fieldValues && Object.prototype.hasOwnProperty.call(this._fieldValues, field.name); } _saveFieldValue(field) { this._savedFieldValues[field.name] = this._getFieldValue(field); } commit() { this._savedFieldValues = {}; this.forEachSubmodel(submodel => { submodel.commit(); }); } rollback() { for (const [name, value] of Object.entries(this._savedFieldValues)) { this._fieldValues[name] = value; } this._savedFieldValues = {}; this.forEachSubmodel(submodel => { submodel.rollback(); }); } isChanged() { return this._isChanged() === true; } _isChanged() { if (!(0, _isEmpty.default)(this._savedFieldValues)) { return true; } return this.forEachSubmodel(submodel => submodel._isChanged()); } fieldIsChanged(field) { if (Object.prototype.hasOwnProperty.call(this._savedFieldValues, field.name)) { return true; } const value = this._getFieldValue(field); if (value !== undefined) { const changedValue = (0, _util2.findFromOneOrMany)(value, value => { if (this.constructor.fieldValueIsSubmodel(value)) { return value.isChanged(); } }); if (changedValue !== undefined) { return true; } } return false; } isNew() { return this._isNew === true; } markAsNew() { this._isNew = true; } markAsNotNew() { this._isNew = false; } static _getRegistry({ throwIfNotFound = true } = {}) { if (!this.$registry && throwIfNotFound) { throw new Error(`Registry not found (model: ${this.getName()})`); } return this.$registry; } // === Validation === validate() { const failedValidators = this.getFailedValidators(); if (failedValidators) { const error = new Error(`Model validation failed (model: '${this.constructor.getName()}', failedValidators: ${JSON.stringify(failedValidators)})`); error.failedValidators = failedValidators; throw error; } return true; } isValid() { return this.getFailedValidators() === undefined; } getFailedValidators() { let result; this.constructor.forEachField(field => { const value = this._getFieldValue(field); const failedValidators = field.validateValue(value); if (!(0, _isEmpty.default)(failedValidators)) { if (!result) { result = {}; } result[field.name] = failedValidators; } }); return result; } // === Remote invocation === static defineRemoteMethod(name, descriptor) { descriptor.value = /*#__PURE__*/ function () { var _ref = _asyncToGenerator(function* (...args) { return this.callRemote(name, ...args); }); return function () { return _ref.apply(this, arguments); }; }(); delete descriptor.initializer; delete descriptor.writable; } static callRemote(methodName, ...args) { var _this = this; return _asyncToGenerator(function* () { const remoteRegistry = _this._getRemoteRegistry(); const { result } = yield remoteRegistry.invokeQuery({ [`${_this.getName()}=>`]: { [`${methodName}=>result`]: { '([])': args } } }); return result; })(); } callRemote(methodName, ...args) { var _this2 = this; return _asyncToGenerator(function* () { const remoteRegistry = _this2.constructor._getRemoteRegistry(); const { result, changes } = yield remoteRegistry.invokeQuery({ '<=': _this2, [`${methodName}=>result`]: { '([])': args }, '=>changes': true }); _this2.assign(changes); return result; })(); } static getName() { return this.name; } // === Utilities === clone() { return this.constructor.deserialize(this.serialize()); } assign(object) { if (object === undefined) {// NOOP } else if (object.isOfType && object.isOfType('Model')) { this._assignOther(object); } else { this._assignObject(object); } } _assignObject(object) { for (const [name, value] of Object.entries(object)) { const field = this.constructor.getField(name); if (field) { this._setFieldValue(field, value); } } } _assignOther(other) { this.constructor.forEachField(field => { const otherField = other.constructor.getField(field.name); if (otherField && other._fieldHasBeenSet(otherField)) { const value = other._getFieldValue(otherField); this._setFieldValue(field, value); } }); } [_util.inspect.custom]() { const object = {}; this.constructor.forEachField(field => { const value = this._getFieldValue(field); if (value !== undefined) { object[field.name] = value; } }); return object; } isOfType(name) { if (name === 'Model') { return true; // Optimization } let Model = this.constructor; while (Model) { if (Model.name === name) { return true; } Model = Object.getPrototypeOf(Model); } return false; } static _getRemoteRegistry() { const registry = this._getRegistry(); if (!registry.remoteRegistry) { throw new Error(`Remote registry not found (model: ${this.name})`); } return registry.remoteRegistry; } } // === Decorators === exports.Model = Model; function field(type, options) { return function (target, name, descriptor) { target.constructor.defineField(name, type, options, descriptor); }; } function remoteMethod() { return function (target, name, descriptor) { if (!(typeof target === 'function')) { // The target is the prototype target = target.constructor; } target.defineRemoteMethod(name, descriptor); }; }