Source: schema.js

  1. const _ = require('lodash');
  2. const clone = require('clone');
  3. import { normalizeProperties } from './normalize'
  4. export class Schema {
  5. /**
  6. * @classdesc Schema class represents the schema definition. It includes properties, methods, static methods, and any
  7. * middleware we want to define.
  8. *
  9. * @description Creates an object schema
  10. * @class
  11. * @api public
  12. * @param {Object} descriptor
  13. * @param {Object} options
  14. * @param {Boolean} options.strict - By default (<code>true</code>), allow only values in the schema to be set.
  15. * When this is <code>false</code>, setting new fields will dynamically add the field
  16. * to the schema as type "any".
  17. * @param {Boolean} options.dotNotation - Allow fields to be set via dot notation. Default: <code>true</code>.
  18. * <code>obj['user.name'] = 'Joe'; -> obj: { user: 'Joe' }<code>
  19. *
  20. * @param {Boolean} options.minimize - "minimize" schemas by removing empty objects. Default: <code>true</code>
  21. * @param {Object} options.toObject - <code>toObject</code> method options.
  22. * @param {Boolean} options.toObject.minimize - "minimize" schemas by removing empty objects. Default: <code>true</code>
  23. * @param {Function} options.toObject.transform - transform function
  24. * @param {Boolean} options.toObject.virtuals - whether to include virtual properties. Default: <code>false</code>
  25. * @param {Boolean} options.toObject.dateToISO - convert dates to string in ISO format using <code>Date.toISOString()</code>. Default: <code>false</code>
  26. * @param {Object} options.toJSON - options for <code>toJSON</code> method options, similar to above
  27. * @param {Boolean} options.strict - ensures that value passed in ot assigned that were not specified in our
  28. * schema do not get saved
  29. * @param {Function} options.onBeforeValueSet - function called when write operations on an object takes place. Currently,
  30. * it will only notify of write operations on the object itself and will not notify you when child objects are written to.
  31. * If you return false or throw an error within the onBeforeValueSet handler, the write operation will be cancelled.
  32. * Throwing an error will add the error to the error stack.
  33. * @param {Function} options.onValueSet - Similar to <code>onBeforeValueSet</code>, but called after we've set a value on the key,
  34. *
  35. * @example
  36. * var schema = new plaster.Schema({ name: String });
  37. * @example <caption>with <code>onBeforeValueSet</code></caption>
  38. * var User = plaster.schema({ name: String }, {
  39. * onBeforeValueSet: function(key, value) {
  40. * if(key === 'name' && value.indexOf('Joe') >= 0) {
  41. * return false;
  42. * });
  43. * }
  44. * });
  45. *
  46. * var User = plaster.model('User', schema);
  47. * var user = new User();
  48. * user.name = 'Bill'; // name not set
  49. * user.name = 'Joe Smith'; // { name: 'Joe Smith' }
  50. */
  51. constructor(descriptor, options = {}) {
  52. // Create object for options if doesn't exist and merge with defaults.
  53. this.options = _.extend({
  54. strict: true,
  55. dotNotation: true
  56. }, options);
  57. this.methods = {};
  58. this.statics = {};
  59. this.callQueue = [];
  60. this.add(descriptor);
  61. }
  62. /**
  63. * Creates a instance method for the created model.
  64. * An object of function names and functions can also be passed in.
  65. *
  66. * @api public
  67. * @param {String} name the name of the method
  68. * @param {Function} func the actual function implementation
  69. *
  70. * @example
  71. * var userSchema = plaster.schema({
  72. * firstName: String,
  73. * lastName: String
  74. * });
  75. *
  76. * userSchema.method('getFullName', function () {
  77. * return this.firstName + ' ' + this.lastName
  78. * });
  79. *
  80. * var User = plaster.model('User', userSchema);
  81. * var user = new User({
  82. * firstName: 'Joe',
  83. * lastName: 'Smith'
  84. * });
  85. *
  86. * console.log(user.getFullName()); // Joe Smith
  87. *
  88. */
  89. method(name, func) {
  90. if (_.isPlainObject(name)) {
  91. for (func in name) {
  92. this.method(func, name[func]);
  93. }
  94. }
  95. else {
  96. if (!_.isString(name)) throw new TypeError('Schema#method expects a string identifier as a function name');
  97. else if (!_.isFunction(func)) throw new TypeError('Schema#method expects a function as a handle');
  98. this.methods[name] = func;
  99. }
  100. }
  101. /**
  102. * Creates a static function for the created model.
  103. * An object of function names and functions can also be passed in.
  104. *
  105. * @api public
  106. * @param {String} name name of the statuc function
  107. * @param {Function} func the actual function
  108. *
  109. * * @example
  110. * var userSchema = plaster.schema({ name: String });
  111. *
  112. * userSchema.static('foo', function () {
  113. * return 'bar';
  114. * });
  115. *
  116. * var User = plaster.model('User', userSchema);
  117. *
  118. * console.log(User.foo()); // 'bar'
  119. *
  120. */
  121. static(name, func) {
  122. if (_.isPlainObject(name)) {
  123. for (func in name) {
  124. this.statics(func, name[func]);
  125. }
  126. }
  127. else {
  128. if (!_.isString(name)) throw new TypeError('Schema#statics expects a string identifier as a function name');
  129. else if (!_.isFunction(func)) throw new TypeError('Schema#statics expects a function as a handle');
  130. this.statics[name] = func;
  131. }
  132. }
  133. /**
  134. * Creates a virtual property for the created model with the given object
  135. * specifying the get and optionally set function
  136. *
  137. * @api public
  138. * @param {String} name name of the virtual property
  139. * @param {String|Function|Object} type optional type to be used for the virtual property. If not provided default is
  140. * 'any' type.
  141. * @param {Object} options virtual options
  142. * @param {Function} options.get - the virtual getter function
  143. * @param {Function} options.set - the virtual setter function. If not provided the virtual becomes read-only.
  144. *
  145. * @example
  146. * var userSchema = plaster.schema({firstName: String, lastName: String});
  147. *
  148. * userSchema.virtual('fullName', String, {
  149. * get: function () {
  150. * return this.firstName + ' ' + this.lastName;
  151. * },
  152. * set: function (v) {
  153. * if (v !== undefined) {
  154. * var parts = v.split(' ');
  155. * this.firstName = parts[0];
  156. * this.lastName = parts[1];
  157. * }
  158. * }
  159. * });
  160. *
  161. * var User = plaster.model('User', userSchema);
  162. *
  163. * var user = new User({firstName: 'Joe', lastName: 'Smith'});
  164. * console.log(user.fullName); // Joe Smith
  165. * user.fullName = 'Bill Jones';
  166. * console.log(user.firstName); // Bill
  167. * console.log(user.lastName); // Jones
  168. * console.log(user.fullName); // Bill Jones
  169. */
  170. virtual(name, type, options) {
  171. if (!_.isString(name)) throw new TypeError('Schema#virtual expects a string identifier as a property name');
  172. if (_.isPlainObject(type) && !options) {
  173. options = type;
  174. type = 'any';
  175. }
  176. else if (!_.isPlainObject(options)) throw new TypeError('Schema#virtual expects an object as a handle');
  177. else if (!_.isFunction(options.get)) throw new TypeError('Schema#virtual expects an object with a get function');
  178. var virtualType = {
  179. type: type,
  180. virtual: true,
  181. get: options.get,
  182. invisible: true
  183. };
  184. if (options.set) {
  185. virtualType.set = options.set;
  186. }
  187. else {
  188. virtualType.readOnly = true;
  189. }
  190. this.descriptor[name] = virtualType;
  191. }
  192. /**
  193. * Sets/gets a schema option.
  194. *
  195. * @param {String} key option name
  196. * @param {Object} [value] if not passed, the current option value is returned
  197. * @api public
  198. */
  199. set(key, value) {
  200. if (1 === arguments.length) {
  201. return this.options[key];
  202. }
  203. this.options[key] = value;
  204. return this;
  205. }
  206. /**
  207. * Gets a schema option.
  208. *
  209. * @api public
  210. * @param {String} key option name
  211. * @return {*} the option value
  212. */
  213. get(key) {
  214. return this.options[key];
  215. }
  216. /**
  217. * Defines a pre hook for the schema.
  218. * See {@link https://www.npmjs.com/package/hooks-fixed hooks-fixed}.
  219. */
  220. pre() {
  221. return this.queue('pre', arguments);
  222. }
  223. /**
  224. * Defines a post hook for the schema.
  225. * See {@link https://www.npmjs.com/package/hooks-fixed hooks-fixed}.
  226. */
  227. post() {
  228. return this.queue('post', arguments);
  229. }
  230. /**
  231. * Adds a method call to the queue.
  232. *
  233. * @param {String} name name of the document method to call later
  234. * @param {Array} args arguments to pass to the method
  235. * @api private
  236. */
  237. queue(name, args) {
  238. var q = {hook: name, args: args};
  239. if (args[0] && typeof args[0] === 'string') {
  240. q.hooked = args[0];
  241. }
  242. this.callQueue.push(q);
  243. }
  244. /**
  245. * Adds the descriptor to the schema at the given key
  246. * @param key the property key
  247. * @param descriptor the property descriptor
  248. *
  249. * @example
  250. * var userSchema = plaster.schema({firstName: String });
  251. * userSchema.add('lastName', String);
  252. */
  253. add(key, descriptor) {
  254. if (!this.descriptor) {
  255. this.descriptor = {};
  256. }
  257. // adjust our descriptor
  258. if (key && descriptor) {
  259. this.descriptor[key] = normalizeProperties.call(this, descriptor, key)
  260. }
  261. else if (typeof key === 'object' && !descriptor) {
  262. if (!this.descriptor) {
  263. this.descriptor = {};
  264. }
  265. _.each(key, (properties, index) => {
  266. this.descriptor[index] = normalizeProperties.call(this, properties, index);
  267. });
  268. }
  269. }
  270. /**
  271. * Clones property from other to us
  272. * @param {Schema} other - other schema
  273. * @param {String} prop - property name
  274. * @param {Boolean} add - whether to add() or assign. if true will do deep clone.
  275. * @private
  276. */
  277. _cloneProp(other, prop, add) {
  278. if (other && other[prop]) {
  279. let p;
  280. for (p in other[prop]) {
  281. if (other[prop].hasOwnProperty(p) && !this[prop][p]) {
  282. if (add) {
  283. this.add(p, clone(other.descriptor[p]));
  284. }
  285. else {
  286. this[prop][p] = other[prop][p];
  287. }
  288. }
  289. }
  290. }
  291. }
  292. /**
  293. * Extends other schema. Copies descriptor properties, methods, statics, virtuals and middleware.
  294. * If this schema has a named property already, the property is not copied.
  295. * @param {Schema} other the schema to extend.
  296. */
  297. extend(other) {
  298. if (other && other instanceof Schema) {
  299. this._cloneProp(other, 'descriptor', true);
  300. this._cloneProp(other, 'statics');
  301. this._cloneProp(other, 'methods');
  302. var self = this;
  303. other.callQueue.forEach(function (e) {
  304. self.callQueue.unshift(e);
  305. });
  306. }
  307. return this;
  308. }
  309. }