compile.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. 'use strict';
  2. const clone = require('../../helpers/clone');
  3. const documentSchemaSymbol = require('../../helpers/symbols').documentSchemaSymbol;
  4. const internalToObjectOptions = require('../../options').internalToObjectOptions;
  5. const utils = require('../../utils');
  6. let Document;
  7. const getSymbol = require('../../helpers/symbols').getSymbol;
  8. const scopeSymbol = require('../../helpers/symbols').scopeSymbol;
  9. const isPOJO = utils.isPOJO;
  10. /*!
  11. * exports
  12. */
  13. exports.compile = compile;
  14. exports.defineKey = defineKey;
  15. const _isEmptyOptions = Object.freeze({
  16. minimize: true,
  17. virtuals: false,
  18. getters: false,
  19. transform: false
  20. });
  21. const noDottedPathGetOptions = Object.freeze({
  22. noDottedPath: true
  23. });
  24. /**
  25. * Compiles schemas.
  26. * @param {Object} tree
  27. * @param {Any} proto
  28. * @param {String} prefix
  29. * @param {Object} options
  30. * @api private
  31. */
  32. function compile(tree, proto, prefix, options) {
  33. Document = Document || require('../../document');
  34. const typeKey = options.typeKey;
  35. for (const key of Object.keys(tree)) {
  36. const limb = tree[key];
  37. const hasSubprops = isPOJO(limb) &&
  38. utils.hasOwnKeys(limb) &&
  39. (!limb[typeKey] || (typeKey === 'type' && isPOJO(limb.type) && limb.type.type));
  40. const subprops = hasSubprops ? limb : null;
  41. defineKey({ prop: key, subprops: subprops, prototype: proto, prefix: prefix, options: options });
  42. }
  43. }
  44. /**
  45. * Defines the accessor named prop on the incoming prototype.
  46. * @param {Object} options
  47. * @param {String} options.prop
  48. * @param {Boolean} options.subprops
  49. * @param {Any} options.prototype
  50. * @param {String} [options.prefix]
  51. * @param {Object} options.options
  52. * @api private
  53. */
  54. function defineKey({ prop, subprops, prototype, prefix, options }) {
  55. Document = Document || require('../../document');
  56. const path = (prefix ? prefix + '.' : '') + prop;
  57. prefix = prefix || '';
  58. const useGetOptions = prefix ? Object.freeze({}) : noDottedPathGetOptions;
  59. if (subprops) {
  60. Object.defineProperty(prototype, prop, {
  61. enumerable: true,
  62. configurable: true,
  63. get: function() {
  64. const _this = this;
  65. if (!this.$__.getters) {
  66. this.$__.getters = {};
  67. }
  68. if (!this.$__.getters[path]) {
  69. const nested = Object.create(Document.prototype, getOwnPropertyDescriptors(this));
  70. // save scope for nested getters/setters
  71. if (!prefix) {
  72. nested.$__[scopeSymbol] = this;
  73. }
  74. nested.$__.nestedPath = path;
  75. Object.defineProperty(nested, 'schema', {
  76. enumerable: false,
  77. configurable: true,
  78. writable: false,
  79. value: prototype.schema
  80. });
  81. Object.defineProperty(nested, '$__schema', {
  82. enumerable: false,
  83. configurable: true,
  84. writable: false,
  85. value: prototype.schema
  86. });
  87. Object.defineProperty(nested, documentSchemaSymbol, {
  88. enumerable: false,
  89. configurable: true,
  90. writable: false,
  91. value: prototype.schema
  92. });
  93. Object.defineProperty(nested, 'toObject', {
  94. enumerable: false,
  95. configurable: true,
  96. writable: false,
  97. value: function() {
  98. return clone(_this.get(path, null, {
  99. virtuals: this &&
  100. this.schema &&
  101. this.schema.options &&
  102. this.schema.options.toObject &&
  103. this.schema.options.toObject.virtuals || null
  104. }));
  105. }
  106. });
  107. Object.defineProperty(nested, '$__get', {
  108. enumerable: false,
  109. configurable: true,
  110. writable: false,
  111. value: function() {
  112. return _this.get(path, null, {
  113. virtuals: this?.schema?.options?.toObject?.virtuals || null
  114. });
  115. }
  116. });
  117. Object.defineProperty(nested, 'toJSON', {
  118. enumerable: false,
  119. configurable: true,
  120. writable: false,
  121. value: function() {
  122. return _this.get(path, null, {
  123. virtuals: this?.schema?.options?.toJSON?.virtuals || null
  124. });
  125. }
  126. });
  127. Object.defineProperty(nested, '$__isNested', {
  128. enumerable: false,
  129. configurable: true,
  130. writable: false,
  131. value: true
  132. });
  133. Object.defineProperty(nested, '$isEmpty', {
  134. enumerable: false,
  135. configurable: true,
  136. writable: false,
  137. value: function() {
  138. return Object.keys(this.get(path, null, _isEmptyOptions) || {}).length === 0;
  139. }
  140. });
  141. Object.defineProperty(nested, '$__parent', {
  142. enumerable: false,
  143. configurable: true,
  144. writable: false,
  145. value: this
  146. });
  147. compile(subprops, nested, path, options);
  148. this.$__.getters[path] = nested;
  149. }
  150. return this.$__.getters[path];
  151. },
  152. set: function(v) {
  153. if (v?.$__isNested) {
  154. // Convert top-level to POJO, but leave subdocs hydrated so `$set`
  155. // can handle them. See gh-9293.
  156. v = v.$__get();
  157. } else if (v instanceof Document && !v.$__isNested) {
  158. v = v.$toObject(internalToObjectOptions);
  159. }
  160. const doc = this.$__[scopeSymbol] || this;
  161. doc.$set(path, v);
  162. }
  163. });
  164. } else {
  165. Object.defineProperty(prototype, prop, {
  166. enumerable: true,
  167. configurable: true,
  168. get: function() {
  169. return this[getSymbol].call(
  170. this.$__[scopeSymbol] || this,
  171. path,
  172. null,
  173. useGetOptions
  174. );
  175. },
  176. set: function(v) {
  177. this.$set.call(this.$__[scopeSymbol] || this, path, v);
  178. }
  179. });
  180. }
  181. }
  182. // gets descriptors for all properties of `object`
  183. // makes all properties non-enumerable to match previous behavior to #2211
  184. function getOwnPropertyDescriptors(object) {
  185. const result = {};
  186. Object.getOwnPropertyNames(object).forEach(function(key) {
  187. const skip = [
  188. 'isNew',
  189. '$__',
  190. '$errors',
  191. 'errors',
  192. '_doc',
  193. '$locals',
  194. '$op',
  195. '__parentArray',
  196. '__index',
  197. '$isDocumentArrayElement'
  198. ].indexOf(key) === -1;
  199. if (skip) {
  200. return;
  201. }
  202. result[key] = Object.getOwnPropertyDescriptor(object, key);
  203. result[key].enumerable = false;
  204. });
  205. return result;
  206. }