modify.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. 'use strict';
  2. const Assert = require('@hapi/hoek/lib/assert');
  3. const Common = require('./common');
  4. const Ref = require('./ref');
  5. const internals = {};
  6. exports.Ids = internals.Ids = class {
  7. constructor() {
  8. this._byId = new Map();
  9. this._byKey = new Map();
  10. this._schemaChain = false;
  11. }
  12. clone() {
  13. const clone = new internals.Ids();
  14. clone._byId = new Map(this._byId);
  15. clone._byKey = new Map(this._byKey);
  16. clone._schemaChain = this._schemaChain;
  17. return clone;
  18. }
  19. concat(source) {
  20. if (source._schemaChain) {
  21. this._schemaChain = true;
  22. }
  23. for (const [id, value] of source._byId.entries()) {
  24. Assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id);
  25. this._byId.set(id, value);
  26. }
  27. for (const [key, value] of source._byKey.entries()) {
  28. Assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key);
  29. this._byKey.set(key, value);
  30. }
  31. }
  32. fork(path, adjuster, root) {
  33. const chain = this._collect(path);
  34. chain.push({ schema: root });
  35. const tail = chain.shift();
  36. let adjusted = { id: tail.id, schema: adjuster(tail.schema) };
  37. Assert(Common.isSchema(adjusted.schema), 'adjuster function failed to return a joi schema type');
  38. for (const node of chain) {
  39. adjusted = { id: node.id, schema: internals.fork(node.schema, adjusted.id, adjusted.schema) };
  40. }
  41. return adjusted.schema;
  42. }
  43. labels(path, behind = []) {
  44. const current = path[0];
  45. const node = this._get(current);
  46. if (!node) {
  47. return [...behind, ...path].join('.');
  48. }
  49. const forward = path.slice(1);
  50. behind = [...behind, node.schema._flags.label || current];
  51. if (!forward.length) {
  52. return behind.join('.');
  53. }
  54. return node.schema._ids.labels(forward, behind);
  55. }
  56. reach(path, behind = []) {
  57. const current = path[0];
  58. const node = this._get(current);
  59. Assert(node, 'Schema does not contain path', [...behind, ...path].join('.'));
  60. const forward = path.slice(1);
  61. if (!forward.length) {
  62. return node.schema;
  63. }
  64. return node.schema._ids.reach(forward, [...behind, current]);
  65. }
  66. register(schema, { key } = {}) {
  67. if (!schema ||
  68. !Common.isSchema(schema)) {
  69. return;
  70. }
  71. if (schema.$_property('schemaChain') ||
  72. schema._ids._schemaChain) {
  73. this._schemaChain = true;
  74. }
  75. const id = schema._flags.id;
  76. if (id) {
  77. const existing = this._byId.get(id);
  78. Assert(!existing || existing.schema === schema, 'Cannot add different schemas with the same id:', id);
  79. Assert(!this._byKey.has(id), 'Schema id conflicts with existing key:', id);
  80. this._byId.set(id, { schema, id });
  81. }
  82. if (key) {
  83. Assert(!this._byKey.has(key), 'Schema already contains key:', key);
  84. Assert(!this._byId.has(key), 'Schema key conflicts with existing id:', key);
  85. this._byKey.set(key, { schema, id: key });
  86. }
  87. }
  88. reset() {
  89. this._byId = new Map();
  90. this._byKey = new Map();
  91. this._schemaChain = false;
  92. }
  93. _collect(path, behind = [], nodes = []) {
  94. const current = path[0];
  95. const node = this._get(current);
  96. Assert(node, 'Schema does not contain path', [...behind, ...path].join('.'));
  97. nodes = [node, ...nodes];
  98. const forward = path.slice(1);
  99. if (!forward.length) {
  100. return nodes;
  101. }
  102. return node.schema._ids._collect(forward, [...behind, current], nodes);
  103. }
  104. _get(id) {
  105. return this._byId.get(id) || this._byKey.get(id);
  106. }
  107. };
  108. internals.fork = function (schema, id, replacement) {
  109. const each = (item, { key }) => {
  110. if (id === (item._flags.id || key)) {
  111. return replacement;
  112. }
  113. };
  114. const obj = exports.schema(schema, { each, ref: false });
  115. return obj ? obj.$_mutateRebuild() : schema;
  116. };
  117. exports.schema = function (schema, options) {
  118. let obj;
  119. for (const name in schema._flags) {
  120. if (name[0] === '_') {
  121. continue;
  122. }
  123. const result = internals.scan(schema._flags[name], { source: 'flags', name }, options);
  124. if (result !== undefined) {
  125. obj = obj || schema.clone();
  126. obj._flags[name] = result;
  127. }
  128. }
  129. for (let i = 0; i < schema._rules.length; ++i) {
  130. const rule = schema._rules[i];
  131. const result = internals.scan(rule.args, { source: 'rules', name: rule.name }, options);
  132. if (result !== undefined) {
  133. obj = obj || schema.clone();
  134. const clone = Object.assign({}, rule);
  135. clone.args = result;
  136. obj._rules[i] = clone;
  137. const existingUnique = obj._singleRules.get(rule.name);
  138. if (existingUnique === rule) {
  139. obj._singleRules.set(rule.name, clone);
  140. }
  141. }
  142. }
  143. for (const name in schema.$_terms) {
  144. if (name[0] === '_') {
  145. continue;
  146. }
  147. const result = internals.scan(schema.$_terms[name], { source: 'terms', name }, options);
  148. if (result !== undefined) {
  149. obj = obj || schema.clone();
  150. obj.$_terms[name] = result;
  151. }
  152. }
  153. return obj;
  154. };
  155. internals.scan = function (item, source, options, _path, _key) {
  156. const path = _path || [];
  157. if (item === null ||
  158. typeof item !== 'object') {
  159. return;
  160. }
  161. let clone;
  162. if (Array.isArray(item)) {
  163. for (let i = 0; i < item.length; ++i) {
  164. const key = source.source === 'terms' && source.name === 'keys' && item[i].key;
  165. const result = internals.scan(item[i], source, options, [i, ...path], key);
  166. if (result !== undefined) {
  167. clone = clone || item.slice();
  168. clone[i] = result;
  169. }
  170. }
  171. return clone;
  172. }
  173. if (options.schema !== false && Common.isSchema(item) ||
  174. options.ref !== false && Ref.isRef(item)) {
  175. const result = options.each(item, { ...source, path, key: _key });
  176. if (result === item) {
  177. return;
  178. }
  179. return result;
  180. }
  181. for (const key in item) {
  182. if (key[0] === '_') {
  183. continue;
  184. }
  185. const result = internals.scan(item[key], source, options, [key, ...path], _key);
  186. if (result !== undefined) {
  187. clone = clone || Object.assign({}, item);
  188. clone[key] = result;
  189. }
  190. }
  191. return clone;
  192. };