discriminator.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. 'use strict';
  2. const Mixed = require('../../schema/mixed');
  3. const ObjectId = require('../../types/objectid');
  4. const defineKey = require('../document/compile').defineKey;
  5. const get = require('../get');
  6. const utils = require('../../utils');
  7. const CUSTOMIZABLE_DISCRIMINATOR_OPTIONS = {
  8. toJSON: true,
  9. toObject: true,
  10. _id: true,
  11. id: true
  12. };
  13. /*!
  14. * ignore
  15. */
  16. module.exports = function discriminator(model, name, schema, tiedValue, applyPlugins) {
  17. if (!(schema && schema.instanceOfSchema)) {
  18. throw new Error('You must pass a valid discriminator Schema');
  19. }
  20. if (model.schema.discriminatorMapping &&
  21. !model.schema.discriminatorMapping.isRoot) {
  22. throw new Error('Discriminator "' + name +
  23. '" can only be a discriminator of the root model');
  24. }
  25. if (applyPlugins) {
  26. const applyPluginsToDiscriminators = get(model.base,
  27. 'options.applyPluginsToDiscriminators', false);
  28. // Even if `applyPluginsToDiscriminators` isn't set, we should still apply
  29. // global plugins to schemas embedded in the discriminator schema (gh-7370)
  30. model.base._applyPlugins(schema, {
  31. skipTopLevel: !applyPluginsToDiscriminators
  32. });
  33. }
  34. const key = model.schema.options.discriminatorKey;
  35. const existingPath = model.schema.path(key);
  36. if (existingPath != null) {
  37. if (!utils.hasUserDefinedProperty(existingPath.options, 'select')) {
  38. existingPath.options.select = true;
  39. }
  40. existingPath.options.$skipDiscriminatorCheck = true;
  41. } else {
  42. const baseSchemaAddition = {};
  43. baseSchemaAddition[key] = {
  44. default: void 0,
  45. select: true,
  46. $skipDiscriminatorCheck: true
  47. };
  48. baseSchemaAddition[key][model.schema.options.typeKey] = String;
  49. model.schema.add(baseSchemaAddition);
  50. defineKey(key, null, model.prototype, null, [key], model.schema.options);
  51. }
  52. if (schema.path(key) && schema.path(key).options.$skipDiscriminatorCheck !== true) {
  53. throw new Error('Discriminator "' + name +
  54. '" cannot have field with name "' + key + '"');
  55. }
  56. let value = name;
  57. if ((typeof tiedValue === 'string' && tiedValue.length) ||
  58. typeof tiedValue === 'number' ||
  59. tiedValue instanceof ObjectId) {
  60. value = tiedValue;
  61. }
  62. function merge(schema, baseSchema) {
  63. // Retain original schema before merging base schema
  64. schema._baseSchema = baseSchema;
  65. if (baseSchema.paths._id &&
  66. baseSchema.paths._id.options &&
  67. !baseSchema.paths._id.options.auto) {
  68. schema.remove('_id');
  69. }
  70. // Find conflicting paths: if something is a path in the base schema
  71. // and a nested path in the child schema, overwrite the base schema path.
  72. // See gh-6076
  73. const baseSchemaPaths = Object.keys(baseSchema.paths);
  74. const conflictingPaths = [];
  75. for (const path of baseSchemaPaths) {
  76. if (schema.nested[path]) {
  77. conflictingPaths.push(path);
  78. continue;
  79. }
  80. if (path.indexOf('.') === -1) {
  81. continue;
  82. }
  83. const sp = path.split('.').slice(0, -1);
  84. let cur = '';
  85. for (const piece of sp) {
  86. cur += (cur.length ? '.' : '') + piece;
  87. if (schema.paths[cur] instanceof Mixed ||
  88. schema.singleNestedPaths[cur] instanceof Mixed) {
  89. conflictingPaths.push(path);
  90. }
  91. }
  92. }
  93. utils.merge(schema, baseSchema, {
  94. isDiscriminatorSchemaMerge: true,
  95. omit: { discriminators: true, base: true },
  96. omitNested: conflictingPaths.reduce((cur, path) => {
  97. cur['tree.' + path] = true;
  98. return cur;
  99. }, {})
  100. });
  101. // Clean up conflicting paths _after_ merging re: gh-6076
  102. for (const conflictingPath of conflictingPaths) {
  103. delete schema.paths[conflictingPath];
  104. }
  105. // Rebuild schema models because schemas may have been merged re: #7884
  106. schema.childSchemas.forEach(obj => {
  107. obj.model.prototype.$__setSchema(obj.schema);
  108. });
  109. const obj = {};
  110. obj[key] = {
  111. default: value,
  112. select: true,
  113. set: function(newName) {
  114. if (newName === value) {
  115. return value;
  116. }
  117. throw new Error('Can\'t set discriminator key "' + key + '"');
  118. },
  119. $skipDiscriminatorCheck: true
  120. };
  121. obj[key][schema.options.typeKey] = existingPath ?
  122. existingPath.instance :
  123. String;
  124. schema.add(obj);
  125. schema.discriminatorMapping = { key: key, value: value, isRoot: false };
  126. if (baseSchema.options.collection) {
  127. schema.options.collection = baseSchema.options.collection;
  128. }
  129. const toJSON = schema.options.toJSON;
  130. const toObject = schema.options.toObject;
  131. const _id = schema.options._id;
  132. const id = schema.options.id;
  133. const keys = Object.keys(schema.options);
  134. schema.options.discriminatorKey = baseSchema.options.discriminatorKey;
  135. for (const _key of keys) {
  136. if (!CUSTOMIZABLE_DISCRIMINATOR_OPTIONS[_key]) {
  137. // Special case: compiling a model sets `pluralization = true` by default. Avoid throwing an error
  138. // for that case. See gh-9238
  139. if (_key === 'pluralization' && schema.options[_key] == true && baseSchema.options[_key] == null) {
  140. continue;
  141. }
  142. if (!utils.deepEqual(schema.options[_key], baseSchema.options[_key])) {
  143. throw new Error('Can\'t customize discriminator option ' + _key +
  144. ' (can only modify ' +
  145. Object.keys(CUSTOMIZABLE_DISCRIMINATOR_OPTIONS).join(', ') +
  146. ')');
  147. }
  148. }
  149. }
  150. schema.options = utils.clone(baseSchema.options);
  151. if (toJSON) schema.options.toJSON = toJSON;
  152. if (toObject) schema.options.toObject = toObject;
  153. if (typeof _id !== 'undefined') {
  154. schema.options._id = _id;
  155. }
  156. schema.options.id = id;
  157. schema.s.hooks = model.schema.s.hooks.merge(schema.s.hooks);
  158. schema.plugins = Array.prototype.slice.call(baseSchema.plugins);
  159. schema.callQueue = baseSchema.callQueue.concat(schema.callQueue);
  160. delete schema._requiredpaths; // reset just in case Schema#requiredPaths() was called on either schema
  161. }
  162. // merges base schema into new discriminator schema and sets new type field.
  163. merge(schema, model.schema);
  164. if (!model.discriminators) {
  165. model.discriminators = {};
  166. }
  167. if (!model.schema.discriminatorMapping) {
  168. model.schema.discriminatorMapping = { key: key, value: null, isRoot: true };
  169. }
  170. if (!model.schema.discriminators) {
  171. model.schema.discriminators = {};
  172. }
  173. model.schema.discriminators[name] = schema;
  174. if (model.discriminators[name]) {
  175. throw new Error('Discriminator with name "' + name + '" already exists');
  176. }
  177. return schema;
  178. };