src/Model/index.js
/* ############################################################################
The MIT License (MIT)
Copyright (c) 2016 - 2019 Van Schroeder
Copyright (c) 2017-2019 Webfreshener, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
############################################################################ */
import {
_oBuilders, _validators, _dirtyModels, _schemaSignatures
} from "./_references";
import {ObserverBuilder} from "./_observerBuilder";
import {PropertiesModel} from "./propertiesModel";
import {ItemsModel} from "./itemsModel";
import {AjvWrapper} from "./_ajvWrapper";
import Notifiers from "./_branchNotifier";
import {walkObject} from "./utils";
const _documents = new WeakMap();
/**
* returns Items or Properties model class based on type of expected property
* @param el
* @returns {ItemsModel|PropertiesModel|Function}
* @private
*/
const _getModelClass = (el) => {
let _class = PropertiesModel;
if ((el.hasOwnProperty("type") && el["type"] === "array") ||
(el.hasOwnProperty("items") && Array.isArray(el["items"]))) {
_class = ItemsModel;
}
return _class;
};
/**
* RxVo Model Entry-point
* @public
*/
export class Model {
/**
* @constructor
* @param {json} schemas
* @param {object} options
*/
constructor(schemas, options = {}) {
// attempts to get user passes Avj options
let ajvOptions = options.hasOwnProperty("ajvOptions") ?
options["ajvOptions"] : null;
// defines AjvWrapper instance for this Document and it's Schemas
const _ajv = new AjvWrapper(this, schemas, ajvOptions);
// sets AjvWrapper instance on map for use
_validators.set(this, _ajv);
// throws error if error message returned
if (!_ajv.$ajv.validateSchema({schemas: schemas["schemas"]}, false)) {
throw _ajv.$ajv.errors;
}
_schemaSignatures.set(this, schemas);
_oBuilders.set(this, new ObserverBuilder());
const _schema = schemas.hasOwnProperty("schemas") ?
schemas.schemas[schemas.schemas.length - 1] : schemas;
const _doc = new (_getModelClass(_schema))(this, _schema.$id || "root#");
// creates holder for dirty model flags in this scope
_dirtyModels.set(this, {});
// applies Subject Handlers to root document
_oBuilders.get(this).create(_doc);
// sets document to this scope
_documents.set(this, _doc);
// creates RxJS Notification Delegate
Notifiers.create(this);
}
/**
* Adds Schema to Validator instance
* @param schema
* @returns {boolean}
*/
addSchema(schema) {
_validators.get(this).$ajv.addSchema(schema);
return (_validators.get(this).$ajv.errors === null);
}
/**
* Selects schema to validate against (advanced option, use wisely)
* @param id
*/
useSchema(id) {
_validators.get(this).$ajv.getSchema(id);
}
/**
* Getter for root Model
* @returns {object|array}
*/
get model() {
return _documents.get(this).model;
}
/**
* Setter for root Model value
* @param {object|array} value
*/
set model(value) {
_documents.get(this).model = value;
}
/**
* Getter for document's JSON-Schema
* @return {any}
*/
get schema() {
const _id = _validators.get(this).path;
return this.getSchemaForKey(_id);
}
/**
* Freezes document object
* @returns {Model}
*/
freeze() {
_documents.get(this).freeze();
return this;
}
/**
* Returns true if object is frozen
* @returns {boolean}
*/
get isFrozen() {
return _documents.get(this).isFrozen;
}
/**
* returns schema with given id
* @param id
* @returns {*}
*/
getSchemaForKey(id) {
let _schema = null;
const _schemas = _schemaSignatures.get(this);
_schemas.schemas.some((schema) => {
if (schema.hasOwnProperty("$id")) {
if (schema.$id === id) {
_schema = schema;
return true;
}
} else if (schema.hasOwnProperty("id")) {
if (schema.id === id) {
_schema = schema;
return true;
}
}
return false;
});
return _schema;
}
/**
* Retrieves JSON-Schema element for given Path
* @param path
* @returns {any}
*/
getSchemaForPath(path) {
let _id;
if (path.indexOf("#") > -1) {
const _sp = path.split("#");
_id = _sp[0];
path = _sp[1];
} else {
_id = _validators.get(this).path;
}
const _schema = this.getSchemaForKey(_id);
return walkObject(path, _schema);
}
/**
* Validates data against named schema
* @param path
* @param value
* @return {*|void|RegExpExecArray}
*/
validate(path, value) {
return _validators.get(this).exec(path, value);
}
/**
* Getter for Ajv validation error messages
* @return {error[] | null}
*/
get errors() {
return _validators.get(this).$ajv.errors || null;
}
/**
*
* @param to
* @returns {Object|Array}
*/
getPath(to) {
let _ref = this.model;
to = to.replace(/\/?(properties|items)+\//g, ".").replace(/^\./, "");
(to.split(".")).forEach((step) => {
if (_ref[step]) {
_ref = _ref[step];
}
});
return _ref;
}
/**
* Retrieves all Models at given path
* @param to
* @returns {Array[]|Object[]}
*/
getModelsInPath(to) {
const _steps = [this.model];
let _ref = this.model;
to = to.replace(/\/?(properties|items)+\/?/g, ".");
(to.split(".")
.filter((itm, idx, arr) => arr.indexOf(itm) > -1))
.forEach((step) => {
if (_ref[step]) {
_steps[_steps.length] = _ref = _ref[step];
}
});
return _steps;
}
/**
*
* @param pipesOrSchemas
* @returns {Pipe}
*/
pipeline(...pipesOrSchemas) {
return _documents.get(this).pipeline(...pipesOrSchemas);
}
/**
* Subscribes observer to root Model
* @param observer
* @returns {Observable}
*/
subscribe(observer) {
return _documents.get(this).subscribe(observer);
}
/**
* Subscribes observer to Model at path
* @param path
* @param observer
* @returns {Observable}
*/
subscribeTo(path, observer) {
return _documents.get(this).subscribeTo(path, observer);
}
/**
* Implements toString
* @return {string}
*/
toString() {
return `${this.model.$model}`;
}
/**
* Implements toJSON
* @return {*}
*/
toJSON() {
return this.model.$model.toJSON();
}
/**
* Creates new PropertiesModel from JSON data
* @param {string|json} json -- JSON Object or String
* @param {object} options - Model options object
* @returns {Model}
*/
static fromJSON(json, options) {
// quick peek at json param to ensure it looks ok
const __ = (typeof json).match(/^(string|object)+$/);
if (__) {
// attempts to create Model from JSON or JSON string
return new Model((__[1] === "string") ?
JSON.parse(json) : json, options);
}
// throws error if something didn't look right with the json param
throw new Error("json must be either JSON formatted string or object");
}
}