discriminator.js 5.9 KB

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