@storable/model
Version:
Base class providing typed properties, validation and serialization
497 lines (382 loc) • 11.7 kB
JavaScript
"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);
};
}