discriminator.js 6.5 KB

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