Home Reference Source

src/Model/index.js

  1. /* ############################################################################
  2. The MIT License (MIT)
  3.  
  4. Copyright (c) 2016 - 2019 Van Schroeder
  5. Copyright (c) 2017-2019 Webfreshener, LLC
  6.  
  7. Permission is hereby granted, free of charge, to any person obtaining a copy
  8. of this software and associated documentation files (the "Software"), to deal
  9. in the Software without restriction, including without limitation the rights
  10. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. copies of the Software, and to permit persons to whom the Software is
  12. furnished to do so, subject to the following conditions:
  13.  
  14. The above copyright notice and this permission notice shall be included in all
  15. copies or substantial portions of the Software.
  16.  
  17. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. SOFTWARE.
  24.  
  25. ############################################################################ */
  26. import {
  27. _oBuilders, _validators, _dirtyModels, _schemaSignatures
  28. } from "./_references";
  29. import {ObserverBuilder} from "./_observerBuilder";
  30. import {PropertiesModel} from "./propertiesModel";
  31. import {ItemsModel} from "./itemsModel";
  32. import {AjvWrapper} from "./_ajvWrapper";
  33. import Notifiers from "./_branchNotifier";
  34. import {walkObject} from "./utils";
  35.  
  36. const _documents = new WeakMap();
  37. /**
  38. * returns Items or Properties model class based on type of expected property
  39. * @param el
  40. * @returns {ItemsModel|PropertiesModel|Function}
  41. * @private
  42. */
  43. const _getModelClass = (el) => {
  44. let _class = PropertiesModel;
  45.  
  46. if ((el.hasOwnProperty("type") && el["type"] === "array") ||
  47. (el.hasOwnProperty("items") && Array.isArray(el["items"]))) {
  48. _class = ItemsModel;
  49. }
  50.  
  51. return _class;
  52. };
  53.  
  54. /**
  55. * RxVo Model Entry-point
  56. * @public
  57. */
  58. export class Model {
  59. /**
  60. * @constructor
  61. * @param {json} schemas
  62. * @param {object} options
  63. */
  64. constructor(schemas, options = {}) {
  65. // attempts to get user passes Avj options
  66. let ajvOptions = options.hasOwnProperty("ajvOptions") ?
  67. options["ajvOptions"] : null;
  68.  
  69. // defines AjvWrapper instance for this Document and it's Schemas
  70. const _ajv = new AjvWrapper(this, schemas, ajvOptions);
  71.  
  72. // sets AjvWrapper instance on map for use
  73. _validators.set(this, _ajv);
  74.  
  75. // throws error if error message returned
  76. if (!_ajv.$ajv.validateSchema({schemas: schemas["schemas"]}, false)) {
  77. throw _ajv.$ajv.errors;
  78. }
  79.  
  80. _schemaSignatures.set(this, schemas);
  81. _oBuilders.set(this, new ObserverBuilder());
  82.  
  83. const _schema = schemas.hasOwnProperty("schemas") ?
  84. schemas.schemas[schemas.schemas.length - 1] : schemas;
  85.  
  86. const _doc = new (_getModelClass(_schema))(this, _schema.$id || "root#");
  87.  
  88. // creates holder for dirty model flags in this scope
  89. _dirtyModels.set(this, {});
  90.  
  91. // applies Subject Handlers to root document
  92. _oBuilders.get(this).create(_doc);
  93.  
  94. // sets document to this scope
  95. _documents.set(this, _doc);
  96.  
  97. // creates RxJS Notification Delegate
  98. Notifiers.create(this);
  99. }
  100.  
  101. /**
  102. * Adds Schema to Validator instance
  103. * @param schema
  104. * @returns {boolean}
  105. */
  106. addSchema(schema) {
  107. _validators.get(this).$ajv.addSchema(schema);
  108. return (_validators.get(this).$ajv.errors === null);
  109. }
  110.  
  111. /**
  112. * Selects schema to validate against (advanced option, use wisely)
  113. * @param id
  114. */
  115. useSchema(id) {
  116. _validators.get(this).$ajv.getSchema(id);
  117. }
  118.  
  119. /**
  120. * Getter for root Model
  121. * @returns {object|array}
  122. */
  123. get model() {
  124. return _documents.get(this).model;
  125. }
  126.  
  127. /**
  128. * Setter for root Model value
  129. * @param {object|array} value
  130. */
  131. set model(value) {
  132. _documents.get(this).model = value;
  133. }
  134.  
  135. /**
  136. * Getter for document's JSON-Schema
  137. * @return {any}
  138. */
  139. get schema() {
  140. const _id = _validators.get(this).path;
  141. return this.getSchemaForKey(_id);
  142. }
  143.  
  144. /**
  145. * Freezes document object
  146. * @returns {Model}
  147. */
  148. freeze() {
  149. _documents.get(this).freeze();
  150. return this;
  151. }
  152.  
  153. /**
  154. * Returns true if object is frozen
  155. * @returns {boolean}
  156. */
  157. get isFrozen() {
  158. return _documents.get(this).isFrozen;
  159. }
  160.  
  161. /**
  162. * returns schema with given id
  163. * @param id
  164. * @returns {*}
  165. */
  166. getSchemaForKey(id) {
  167. let _schema = null;
  168. const _schemas = _schemaSignatures.get(this);
  169. _schemas.schemas.some((schema) => {
  170. if (schema.hasOwnProperty("$id")) {
  171. if (schema.$id === id) {
  172. _schema = schema;
  173. return true;
  174. }
  175. } else if (schema.hasOwnProperty("id")) {
  176. if (schema.id === id) {
  177. _schema = schema;
  178. return true;
  179. }
  180. }
  181. return false;
  182. });
  183. return _schema;
  184. }
  185.  
  186. /**
  187. * Retrieves JSON-Schema element for given Path
  188. * @param path
  189. * @returns {any}
  190. */
  191. getSchemaForPath(path) {
  192. let _id;
  193. if (path.indexOf("#") > -1) {
  194. const _sp = path.split("#");
  195. _id = _sp[0];
  196. path = _sp[1];
  197. } else {
  198. _id = _validators.get(this).path;
  199. }
  200.  
  201. const _schema = this.getSchemaForKey(_id);
  202.  
  203. return walkObject(path, _schema);
  204. }
  205.  
  206. /**
  207. * Validates data against named schema
  208. * @param path
  209. * @param value
  210. * @return {*|void|RegExpExecArray}
  211. */
  212. validate(path, value) {
  213. return _validators.get(this).exec(path, value);
  214. }
  215.  
  216. /**
  217. * Getter for Ajv validation error messages
  218. * @return {error[] | null}
  219. */
  220. get errors() {
  221. return _validators.get(this).$ajv.errors || null;
  222. }
  223.  
  224. /**
  225. *
  226. * @param to
  227. * @returns {Object|Array}
  228. */
  229. getPath(to) {
  230. let _ref = this.model;
  231. to = to.replace(/\/?(properties|items)+\//g, ".").replace(/^\./, "");
  232. (to.split(".")).forEach((step) => {
  233. if (_ref[step]) {
  234. _ref = _ref[step];
  235. }
  236. });
  237.  
  238. return _ref;
  239. }
  240.  
  241. /**
  242. * Retrieves all Models at given path
  243. * @param to
  244. * @returns {Array[]|Object[]}
  245. */
  246. getModelsInPath(to) {
  247. const _steps = [this.model];
  248. let _ref = this.model;
  249. to = to.replace(/\/?(properties|items)+\/?/g, ".");
  250. (to.split(".")
  251. .filter((itm, idx, arr) => arr.indexOf(itm) > -1))
  252. .forEach((step) => {
  253. if (_ref[step]) {
  254. _steps[_steps.length] = _ref = _ref[step];
  255. }
  256. });
  257. return _steps;
  258. }
  259.  
  260.  
  261. /**
  262. *
  263. * @param pipesOrSchemas
  264. * @returns {Pipe}
  265. */
  266. pipeline(...pipesOrSchemas) {
  267. return _documents.get(this).pipeline(...pipesOrSchemas);
  268. }
  269.  
  270.  
  271. /**
  272. * Subscribes observer to root Model
  273. * @param observer
  274. * @returns {Observable}
  275. */
  276. subscribe(observer) {
  277. return _documents.get(this).subscribe(observer);
  278. }
  279.  
  280. /**
  281. * Subscribes observer to Model at path
  282. * @param path
  283. * @param observer
  284. * @returns {Observable}
  285. */
  286. subscribeTo(path, observer) {
  287. return _documents.get(this).subscribeTo(path, observer);
  288. }
  289.  
  290. /**
  291. * Implements toString
  292. * @return {string}
  293. */
  294. toString() {
  295. return `${this.model.$model}`;
  296. }
  297.  
  298. /**
  299. * Implements toJSON
  300. * @return {*}
  301. */
  302. toJSON() {
  303. return this.model.$model.toJSON();
  304. }
  305.  
  306. /**
  307. * Creates new PropertiesModel from JSON data
  308. * @param {string|json} json -- JSON Object or String
  309. * @param {object} options - Model options object
  310. * @returns {Model}
  311. */
  312. static fromJSON(json, options) {
  313. // quick peek at json param to ensure it looks ok
  314. const __ = (typeof json).match(/^(string|object)+$/);
  315.  
  316. if (__) {
  317. // attempts to create Model from JSON or JSON string
  318. return new Model((__[1] === "string") ?
  319. JSON.parse(json) : json, options);
  320. }
  321.  
  322. // throws error if something didn't look right with the json param
  323. throw new Error("json must be either JSON formatted string or object");
  324. }
  325. }