z-schema 10.0.0 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +35 -17
  2. package/cjs/index.d.ts +345 -34
  3. package/cjs/index.js +4446 -1685
  4. package/dist/errors.js +5 -0
  5. package/dist/format-validators.js +131 -107
  6. package/dist/json-schema-versions.js +4 -1
  7. package/dist/json-schema.js +50 -16
  8. package/dist/json-validation.js +524 -669
  9. package/dist/report.js +37 -16
  10. package/dist/schema-cache.js +76 -18
  11. package/dist/schema-compiler.js +72 -47
  12. package/dist/schema-validator.js +117 -52
  13. package/dist/schemas/draft-07-schema.json +172 -0
  14. package/dist/schemas/draft-2019-09-meta-applicator.json +52 -0
  15. package/dist/schemas/draft-2019-09-meta-content.json +12 -0
  16. package/dist/schemas/draft-2019-09-meta-core.json +53 -0
  17. package/dist/schemas/draft-2019-09-meta-format.json +10 -0
  18. package/dist/schemas/draft-2019-09-meta-meta-data.json +32 -0
  19. package/dist/schemas/draft-2019-09-meta-validation.json +94 -0
  20. package/dist/schemas/draft-2019-09-schema.json +41 -0
  21. package/dist/schemas/draft-2020-12-meta-applicator.json +44 -0
  22. package/dist/schemas/draft-2020-12-meta-content.json +12 -0
  23. package/dist/schemas/draft-2020-12-meta-core.json +47 -0
  24. package/dist/schemas/draft-2020-12-meta-format-annotation.json +10 -0
  25. package/dist/schemas/draft-2020-12-meta-format-assertion.json +10 -0
  26. package/dist/schemas/draft-2020-12-meta-meta-data.json +32 -0
  27. package/dist/schemas/draft-2020-12-meta-unevaluated.json +11 -0
  28. package/dist/schemas/draft-2020-12-meta-validation.json +94 -0
  29. package/dist/schemas/draft-2020-12-schema.json +57 -0
  30. package/dist/types/errors.d.ts +4 -0
  31. package/dist/types/index.d.ts +2 -1
  32. package/dist/types/json-schema-versions.d.ts +128 -9
  33. package/dist/types/json-schema.d.ts +28 -11
  34. package/dist/types/json-validation.d.ts +2 -3
  35. package/dist/types/report.d.ts +14 -4
  36. package/dist/types/schema-cache.d.ts +7 -0
  37. package/dist/types/schema-compiler.d.ts +5 -3
  38. package/dist/types/schema-validator.d.ts +2 -2
  39. package/dist/types/utils/array.d.ts +8 -1
  40. package/dist/types/utils/base64.d.ts +2 -0
  41. package/dist/types/utils/clone.d.ts +1 -1
  42. package/dist/types/utils/date.d.ts +1 -0
  43. package/dist/types/utils/hostname.d.ts +2 -0
  44. package/dist/types/utils/json.d.ts +2 -1
  45. package/dist/types/utils/properties.d.ts +0 -1
  46. package/dist/types/utils/time.d.ts +12 -0
  47. package/dist/types/utils/unicode.d.ts +3 -12
  48. package/dist/types/validation/array.d.ts +12 -0
  49. package/dist/types/validation/combinators.d.ts +10 -0
  50. package/dist/types/validation/numeric.d.ts +8 -0
  51. package/dist/types/validation/object.d.ts +13 -0
  52. package/dist/types/validation/ref.d.ts +11 -0
  53. package/dist/types/validation/shared.d.ts +26 -0
  54. package/dist/types/validation/string.d.ts +9 -0
  55. package/dist/types/validation/type.d.ts +6 -0
  56. package/dist/types/z-schema-base.d.ts +39 -1
  57. package/dist/types/z-schema-options.d.ts +3 -0
  58. package/dist/types/z-schema.d.ts +144 -8
  59. package/dist/utils/array.js +49 -7
  60. package/dist/utils/base64.js +29 -0
  61. package/dist/utils/clone.js +13 -12
  62. package/dist/utils/date.js +21 -0
  63. package/dist/utils/hostname.js +146 -0
  64. package/dist/utils/json.js +11 -6
  65. package/dist/utils/properties.js +1 -6
  66. package/dist/utils/time.js +50 -0
  67. package/dist/utils/unicode.js +8 -41
  68. package/dist/utils/uri.js +1 -1
  69. package/dist/validation/array.js +128 -0
  70. package/dist/validation/combinators.js +107 -0
  71. package/dist/validation/numeric.js +97 -0
  72. package/dist/validation/object.js +238 -0
  73. package/dist/validation/ref.js +70 -0
  74. package/dist/validation/shared.js +136 -0
  75. package/dist/validation/string.js +178 -0
  76. package/dist/validation/type.js +55 -0
  77. package/dist/z-schema-base.js +52 -32
  78. package/dist/z-schema-options.js +12 -8
  79. package/dist/z-schema-versions.js +92 -9
  80. package/dist/z-schema.js +135 -38
  81. package/package.json +22 -8
  82. package/src/errors.ts +8 -0
  83. package/src/format-validators.ts +146 -105
  84. package/src/index.ts +10 -1
  85. package/src/json-schema-versions.ts +181 -11
  86. package/src/json-schema.ts +102 -35
  87. package/src/json-validation.ts +653 -724
  88. package/src/report.ts +42 -20
  89. package/src/schema-cache.ts +94 -18
  90. package/src/schema-compiler.ts +94 -51
  91. package/src/schema-validator.ts +132 -56
  92. package/src/schemas/draft-07-schema.json +172 -0
  93. package/src/schemas/draft-2019-09-meta-applicator.json +53 -0
  94. package/src/schemas/draft-2019-09-meta-content.json +14 -0
  95. package/src/schemas/draft-2019-09-meta-core.json +54 -0
  96. package/src/schemas/draft-2019-09-meta-format.json +11 -0
  97. package/src/schemas/draft-2019-09-meta-meta-data.json +34 -0
  98. package/src/schemas/draft-2019-09-meta-validation.json +95 -0
  99. package/src/schemas/draft-2019-09-schema.json +42 -0
  100. package/src/schemas/draft-2020-12-meta-applicator.json +45 -0
  101. package/src/schemas/draft-2020-12-meta-content.json +14 -0
  102. package/src/schemas/draft-2020-12-meta-core.json +48 -0
  103. package/src/schemas/draft-2020-12-meta-format-annotation.json +11 -0
  104. package/src/schemas/draft-2020-12-meta-format-assertion.json +11 -0
  105. package/src/schemas/draft-2020-12-meta-meta-data.json +34 -0
  106. package/src/schemas/draft-2020-12-meta-unevaluated.json +12 -0
  107. package/src/schemas/draft-2020-12-meta-validation.json +95 -0
  108. package/src/schemas/draft-2020-12-schema.json +58 -0
  109. package/src/utils/array.ts +51 -7
  110. package/src/utils/base64.ts +32 -0
  111. package/src/utils/clone.ts +16 -12
  112. package/src/utils/date.ts +23 -0
  113. package/src/utils/hostname.ts +174 -0
  114. package/src/utils/json.ts +15 -6
  115. package/src/utils/properties.ts +1 -7
  116. package/src/utils/time.ts +73 -0
  117. package/src/utils/unicode.ts +8 -39
  118. package/src/utils/uri.ts +1 -1
  119. package/src/validation/array.ts +158 -0
  120. package/src/validation/combinators.ts +132 -0
  121. package/src/validation/numeric.ts +120 -0
  122. package/src/validation/object.ts +318 -0
  123. package/src/validation/ref.ts +85 -0
  124. package/src/validation/shared.ts +191 -0
  125. package/src/validation/string.ts +224 -0
  126. package/src/validation/type.ts +66 -0
  127. package/src/z-schema-base.ts +54 -36
  128. package/src/z-schema-options.ts +15 -8
  129. package/src/z-schema-versions.ts +107 -12
  130. package/src/z-schema.ts +158 -42
  131. package/umd/ZSchema.js +4446 -1685
  132. package/umd/ZSchema.min.js +1 -1
  133. package/dist/schemas/draft-04-hyper-schema.json +0 -135
  134. package/dist/schemas/draft-06-hyper-schema.json +0 -132
  135. package/dist/schemas/draft-06-links.json +0 -43
  136. package/src/schemas/draft-04-hyper-schema.json +0 -136
  137. package/src/schemas/draft-06-hyper-schema.json +0 -133
  138. package/src/schemas/draft-06-links.json +0 -43
package/dist/report.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Errors, getValidateError } from './errors.js';
2
+ import { shallowClone } from './utils/clone.js';
2
3
  import { get } from './utils/json.js';
3
4
  import { jsonSymbol, schemaSymbol } from './utils/symbols.js';
4
5
  import { isAbsoluteUri } from './utils/uri.js';
@@ -6,6 +7,9 @@ import { isObject } from './utils/what-is.js';
6
7
  export class Report {
7
8
  asyncTasks = [];
8
9
  commonErrorMessage;
10
+ __$recursiveAnchorStack = [];
11
+ __$dynamicScopeStack = [];
12
+ __validationResultCache = new Map();
9
13
  errors = [];
10
14
  json;
11
15
  path = [];
@@ -22,11 +26,17 @@ export class Report {
22
26
  // subreport
23
27
  this.reportOptions = reportOptionsOrValidate || {};
24
28
  this.validateOptions = validateOptions || parentOrOptions.validateOptions;
29
+ this.__$recursiveAnchorStack = [...parentOrOptions.__$recursiveAnchorStack];
30
+ this.__$dynamicScopeStack = [...parentOrOptions.__$dynamicScopeStack];
31
+ this.__validationResultCache = parentOrOptions.__validationResultCache;
25
32
  }
26
33
  else {
27
34
  // primary
28
35
  this.reportOptions = {};
29
36
  this.validateOptions = reportOptionsOrValidate || {};
37
+ this.__$recursiveAnchorStack = [];
38
+ this.__$dynamicScopeStack = [];
39
+ this.__validationResultCache = new Map();
30
40
  }
31
41
  }
32
42
  isValid() {
@@ -38,6 +48,25 @@ export class Report {
38
48
  addAsyncTask(fn, args, asyncTaskResultProcessFn) {
39
49
  this.asyncTasks.push([fn, args, asyncTaskResultProcessFn]);
40
50
  }
51
+ /**
52
+ * Like {@link addAsyncTask}, but automatically saves the current `path` and
53
+ * restores it around `processFn`. This eliminates the manual
54
+ * path-save/restore boilerplate that every async-aware validator would
55
+ * otherwise need.
56
+ */
57
+ addAsyncTaskWithPath(fn, args, processFn) {
58
+ const pathBefore = shallowClone(this.path);
59
+ this.asyncTasks.push([
60
+ fn,
61
+ args,
62
+ (result) => {
63
+ const backup = this.path;
64
+ this.path = pathBefore;
65
+ processFn(result);
66
+ this.path = backup;
67
+ },
68
+ ]);
69
+ }
41
70
  getAncestor(id) {
42
71
  if (!this.parentReport) {
43
72
  return undefined;
@@ -50,7 +79,6 @@ export class Report {
50
79
  processAsyncTasks(timeout, callback) {
51
80
  const validationTimeout = timeout || 2000;
52
81
  let tasksCount = this.asyncTasks.length;
53
- let idx = tasksCount;
54
82
  let timedOut = false;
55
83
  const finish = () => {
56
84
  setTimeout(() => {
@@ -73,8 +101,8 @@ export class Report {
73
101
  finish();
74
102
  return;
75
103
  }
76
- while (idx--) {
77
- const [fn, fnArgs, processFn] = this.asyncTasks[idx];
104
+ for (let i = 0; i < this.asyncTasks.length; i++) {
105
+ const [fn, fnArgs, processFn] = this.asyncTasks[i];
78
106
  const respondCallback = respond(processFn);
79
107
  fn(...fnArgs, respondCallback);
80
108
  }
@@ -137,14 +165,12 @@ export class Report {
137
165
  return this.rootSchema.id;
138
166
  }
139
167
  hasError(errCode, errParams) {
140
- let idx = this.errors.length;
141
- while (idx--) {
168
+ for (let idx = 0; idx < this.errors.length; idx++) {
142
169
  if (this.errors[idx].code === errCode) {
143
170
  // assume match
144
171
  let match = true;
145
172
  // check the params too
146
- let idx2 = this.errors[idx].params.length;
147
- while (idx2--) {
173
+ for (let idx2 = 0; idx2 < this.errors[idx].params.length; idx2++) {
148
174
  if (this.errors[idx].params[idx2] !== errParams[idx2]) {
149
175
  match = false;
150
176
  }
@@ -181,8 +207,7 @@ export class Report {
181
207
  throw new Error('No errorMessage known for code ' + errorCode);
182
208
  }
183
209
  params = params || [];
184
- let idx = params.length;
185
- while (idx--) {
210
+ for (let idx = 0; idx < params.length; idx++) {
186
211
  const param = params[idx] === null || isObject(params[idx]) ? JSON.stringify(params[idx]) : params[idx];
187
212
  errorMessage = errorMessage.replace('{' + idx + '}', param.toString());
188
213
  }
@@ -195,7 +220,6 @@ export class Report {
195
220
  schemaId: this.getSchemaId(),
196
221
  keyword: keyword,
197
222
  };
198
- // TODO v8: remove Symbol usage
199
223
  err[schemaSymbol] = schema;
200
224
  err[jsonSymbol] = this.getJson();
201
225
  if (schema && typeof schema === 'string') {
@@ -214,12 +238,9 @@ export class Report {
214
238
  subReports = [subReports];
215
239
  }
216
240
  err.inner = [];
217
- idx = subReports.length;
218
- while (idx--) {
219
- const subReport = subReports[idx];
220
- let idx2 = subReport.errors.length;
221
- while (idx2--) {
222
- err.inner.push(subReport.errors[idx2]);
241
+ for (const subReport of subReports) {
242
+ for (const error of subReport.errors) {
243
+ err.inner.push(error);
223
244
  }
224
245
  }
225
246
  if (err.inner.length === 0) {
@@ -3,6 +3,35 @@ import { Report } from './report.js';
3
3
  import { deepClone } from './utils/clone.js';
4
4
  import { decodeJSONPointer } from './utils/json.js';
5
5
  import { getQueryPath, getRemotePath, isAbsoluteUri } from './utils/uri.js';
6
+ import { normalizeOptions } from './z-schema-options.js';
7
+ const getEffectiveId = (schema) => {
8
+ let id = getId(schema);
9
+ if ((!id || !isAbsoluteUri(id)) && typeof schema.id === 'string' && isAbsoluteUri(schema.id)) {
10
+ id = schema.id;
11
+ }
12
+ return id;
13
+ };
14
+ /**
15
+ * Shared logic for registering a remote reference schema.
16
+ * Used by both the static `ZSchema.setRemoteReference()` (global cache) and
17
+ * the instance `validator.setRemoteReference()` (instance cache).
18
+ */
19
+ export function prepareRemoteSchema(schema, uri, validationOptions, maxCloneDepth) {
20
+ let _schema;
21
+ if (typeof schema === 'string') {
22
+ _schema = JSON.parse(schema);
23
+ }
24
+ else {
25
+ _schema = deepClone(schema, maxCloneDepth);
26
+ }
27
+ if (!_schema.id) {
28
+ _schema.id = uri;
29
+ }
30
+ if (validationOptions) {
31
+ _schema.__$validationOptions = normalizeOptions(validationOptions);
32
+ }
33
+ return _schema;
34
+ }
6
35
  export class SchemaCache {
7
36
  validator;
8
37
  static global_cache = {};
@@ -41,31 +70,29 @@ export class SchemaCache {
41
70
  return this.getSchemaByUri(report, refOrSchema);
42
71
  }
43
72
  // no caching done on this, but we need to return a clone so we can mutate it
44
- return deepClone(refOrSchema);
73
+ return deepClone(refOrSchema, this.validator.options.maxRecursionDepth);
45
74
  }
46
75
  fromCache(path) {
47
76
  let found = this.cache[path];
48
77
  if (found) {
49
- return this.cache[path];
78
+ return found;
50
79
  }
51
- const asClone = (s) => {
52
- if (!s.id || (!isAbsoluteUri(s.id) && isAbsoluteUri(path))) {
53
- s.id = path;
54
- }
55
- return deepClone(s);
56
- };
57
80
  found = SchemaCache.global_cache[path];
58
81
  if (found) {
59
- return asClone(found);
82
+ // Clone once from global cache into instance cache so subsequent lookups
83
+ // never deep-clone again for the same path on this instance.
84
+ const clone = deepClone(found, this.validator.options.maxRecursionDepth);
85
+ if (!clone.id || (!isAbsoluteUri(clone.id) && isAbsoluteUri(path))) {
86
+ clone.id = path;
87
+ }
88
+ this.cache[path] = clone;
89
+ return clone;
60
90
  }
61
91
  return undefined;
62
92
  }
63
93
  getSchemaByUri(report, uri, root) {
64
94
  if (root && !isAbsoluteUri(uri)) {
65
- let rootId = getId(root);
66
- if ((!rootId || !isAbsoluteUri(rootId)) && typeof root.id === 'string' && isAbsoluteUri(root.id)) {
67
- rootId = root.id;
68
- }
95
+ const rootId = getEffectiveId(root);
69
96
  if (rootId && isAbsoluteUri(rootId)) {
70
97
  const hashIndex = rootId.indexOf('#');
71
98
  const rootBase = hashIndex === -1 ? rootId : rootId.slice(0, hashIndex);
@@ -79,20 +106,40 @@ export class SchemaCache {
79
106
  }
80
107
  const remotePath = getRemotePath(uri);
81
108
  const queryPath = getQueryPath(uri);
82
- let result = remotePath ? this.fromCache(remotePath) : root;
109
+ let result;
110
+ let resolvedFromAncestor = false;
111
+ if (remotePath) {
112
+ const ancestorReport = report.getAncestor(remotePath);
113
+ if (ancestorReport?.rootSchema) {
114
+ result = ancestorReport.rootSchema;
115
+ resolvedFromAncestor = true;
116
+ }
117
+ }
118
+ if (!result && root && remotePath) {
119
+ const rootId = getEffectiveId(root);
120
+ const rootRemotePath = rootId ? getRemotePath(rootId) : undefined;
121
+ if (rootRemotePath && rootRemotePath === remotePath) {
122
+ result = root;
123
+ }
124
+ }
125
+ if (!result) {
126
+ result = remotePath ? this.fromCache(remotePath) : root;
127
+ }
83
128
  if (result && remotePath && isAbsoluteUri(remotePath) && (!result.id || !isAbsoluteUri(result.id))) {
84
129
  result.id = remotePath;
85
130
  }
86
131
  if (result && remotePath) {
87
132
  // we need to avoid compiling schemas in a recursive loop
88
- const compileRemote = result !== root;
133
+ const compileRemote = result !== root && !resolvedFromAncestor;
89
134
  // now we need to compile and validate resolved schema (in case it's not already)
90
135
  if (compileRemote) {
91
136
  report.path.push(remotePath);
92
137
  let remoteReport;
138
+ let usesAncestorReport = false;
93
139
  const anscestorReport = result.id ? report.getAncestor(result.id) : undefined;
94
140
  if (anscestorReport) {
95
141
  remoteReport = anscestorReport;
142
+ usesAncestorReport = true;
96
143
  }
97
144
  else {
98
145
  remoteReport = new Report(report);
@@ -103,14 +150,21 @@ export class SchemaCache {
103
150
  // If custom validationOptions were provided to setRemoteReference(),
104
151
  // use them instead of the default options
105
152
  this.validator.options = result.__$validationOptions || this.validator.options;
106
- this.validator.sv.validateSchema(remoteReport, result);
153
+ const parentSchemaUri = typeof result.$schema === 'string' ? getRemotePath(result.$schema) : undefined;
154
+ const currentSchemaUri = report.getSchemaId();
155
+ const parentSchemaIsCompiling = !!parentSchemaUri &&
156
+ parentSchemaUri.length > 0 &&
157
+ (currentSchemaUri === parentSchemaUri || !!report.getAncestor(parentSchemaUri));
158
+ if (!parentSchemaIsCompiling) {
159
+ this.validator.sv.validateSchema(remoteReport, result);
160
+ }
107
161
  }
108
162
  finally {
109
163
  this.validator.options = savedOptions;
110
164
  }
111
165
  }
112
166
  }
113
- const remoteReportIsValid = remoteReport.isValid();
167
+ const remoteReportIsValid = usesAncestorReport ? true : remoteReport.isValid();
114
168
  if (!remoteReportIsValid) {
115
169
  report.addError('REMOTE_NOT_VALID', [uri], remoteReport);
116
170
  }
@@ -120,13 +174,14 @@ export class SchemaCache {
120
174
  }
121
175
  }
122
176
  }
177
+ const resourceRoot = result;
123
178
  if (result && queryPath) {
124
179
  const parts = queryPath.split('/');
125
180
  for (let idx = 0, lim = parts.length; result && idx < lim; idx++) {
126
181
  const key = decodeJSONPointer(parts[idx]);
127
182
  if (idx === 0) {
128
183
  // it's an id
129
- result = findId(result, key);
184
+ result = findId(result, key, remotePath, remotePath, this.validator.options.maxRecursionDepth);
130
185
  }
131
186
  else {
132
187
  // it's a path behind id
@@ -134,6 +189,9 @@ export class SchemaCache {
134
189
  }
135
190
  }
136
191
  }
192
+ if (result && typeof result === 'object' && resourceRoot && typeof resourceRoot === 'object') {
193
+ result.__$resourceRoot = resourceRoot;
194
+ }
137
195
  return result;
138
196
  }
139
197
  }
@@ -1,13 +1,17 @@
1
- import { getId } from './json-schema.js';
1
+ import { getId, isInternalKey, NON_SCHEMA_KEYWORDS } from './json-schema.js';
2
2
  import { Report } from './report.js';
3
3
  import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
4
+ import { DEFAULT_MAX_RECURSION_DEPTH } from './z-schema-options.js';
4
5
  import { getSchemaReader } from './z-schema-reader.js';
5
- export const collectIds = (obj) => {
6
+ export const collectIds = (obj, maxDepth = DEFAULT_MAX_RECURSION_DEPTH) => {
6
7
  const ids = [];
7
- const doNotCollectIdsFrom = ['enum', 'const', 'default', 'examples'];
8
- function walk(node, scope) {
8
+ function walk(node, scope, _depth = 0) {
9
9
  if (typeof node !== 'object' || node == null)
10
10
  return;
11
+ if (_depth >= maxDepth) {
12
+ throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in collectIds. ` +
13
+ 'If your schema is deeply nested and valid, increase the maxRecursionDepth option.');
14
+ }
11
15
  let addedScope = false;
12
16
  const nodeId = getId(node);
13
17
  if (typeof nodeId === 'string') {
@@ -24,7 +28,7 @@ export const collectIds = (obj) => {
24
28
  id.absoluteUri = nodeId;
25
29
  }
26
30
  else if (type === 'root' && typeof node.id === 'string' && isAbsoluteUri(node.id) && node.id !== nodeId) {
27
- id.absoluteUri = resolveIdScope(node.id, nodeId);
31
+ id.absoluteUri = resolveSchemaScopeId(node.id, node, nodeId);
28
32
  }
29
33
  else if (type === 'relative') {
30
34
  id.absoluteParent = scope
@@ -32,7 +36,7 @@ export const collectIds = (obj) => {
32
36
  .slice(-1)[0];
33
37
  if (id.absoluteParent) {
34
38
  const parentUri = id.absoluteParent.absoluteUri || id.absoluteParent.id;
35
- id.absoluteUri = parentUri.split('/').slice(0, -1).concat(id.id).join('/');
39
+ id.absoluteUri = resolveSchemaScopeId(parentUri, node, id.id);
36
40
  }
37
41
  }
38
42
  ids.push(id);
@@ -41,14 +45,14 @@ export const collectIds = (obj) => {
41
45
  }
42
46
  if (Array.isArray(node)) {
43
47
  for (const item of node) {
44
- walk(item, scope);
48
+ walk(item, scope, _depth + 1);
45
49
  }
46
50
  }
47
51
  else {
48
52
  for (const key of Object.keys(node)) {
49
- if (key.indexOf('__$') === 0 || doNotCollectIdsFrom.includes(key))
53
+ if (isInternalKey(key) || NON_SCHEMA_KEYWORDS.includes(key))
50
54
  continue;
51
- walk(node[key], scope);
55
+ walk(node[key], scope, _depth + 1);
52
56
  }
53
57
  }
54
58
  if (addedScope) {
@@ -58,14 +62,18 @@ export const collectIds = (obj) => {
58
62
  walk(obj, []);
59
63
  return ids;
60
64
  };
61
- const doNotCollectReferencesFrom = ['enum', 'const', 'default', 'examples'];
62
- export const collectReferences = (obj, results, scope, path) => {
65
+ export const collectReferences = (obj, results, scope, path, options, maxDepth = DEFAULT_MAX_RECURSION_DEPTH, _depth = 0) => {
63
66
  results = results || [];
64
67
  scope = scope || [];
65
68
  path = path || [];
69
+ options = options || {};
66
70
  if (typeof obj !== 'object' || obj === null) {
67
71
  return results;
68
72
  }
73
+ if (_depth >= maxDepth) {
74
+ throw new Error(`Maximum recursion depth (${maxDepth}) exceeded in collectReferences. ` +
75
+ 'If your schema is deeply nested and valid, increase the maxRecursionDepth option.');
76
+ }
69
77
  const hasRef = typeof obj.$ref === 'string' && typeof obj.__$refResolved === 'undefined';
70
78
  let addedScope = false;
71
79
  const isRootScope = scope.length === 0;
@@ -74,9 +82,9 @@ export const collectReferences = (obj, results, scope, path) => {
74
82
  if (typeof obj.id === 'string' && isAbsoluteUri(obj.id) && (!scopeId || !isAbsoluteUri(scopeId))) {
75
83
  scopeId = obj.id;
76
84
  }
77
- if (typeof scopeId === 'string' && (isRootScope || !hasRef)) {
85
+ if (typeof scopeId === 'string' && (isRootScope || !hasRef || options.useRefObjectScope === true)) {
78
86
  const base = scope.length > 0 ? scope[scope.length - 1] : undefined;
79
- scope.push(resolveIdScope(base, scopeId));
87
+ scope.push(resolveSchemaScopeId(base, obj, scopeId));
80
88
  addedScope = true;
81
89
  }
82
90
  if (hasRef) {
@@ -87,6 +95,22 @@ export const collectReferences = (obj, results, scope, path) => {
87
95
  path: path.slice(0),
88
96
  });
89
97
  }
98
+ if (typeof obj.$recursiveRef === 'string' && typeof obj.__$recursiveRefResolved === 'undefined') {
99
+ results.push({
100
+ ref: resolveReference(scope[scope.length - 1], obj.$recursiveRef),
101
+ key: '$recursiveRef',
102
+ obj: obj,
103
+ path: path.slice(0),
104
+ });
105
+ }
106
+ if (typeof obj.$dynamicRef === 'string' && typeof obj.__$dynamicRefResolved === 'undefined') {
107
+ results.push({
108
+ ref: resolveReference(scope[scope.length - 1], obj.$dynamicRef),
109
+ key: '$dynamicRef',
110
+ obj: obj,
111
+ path: path.slice(0),
112
+ });
113
+ }
90
114
  if (typeof obj.$schema === 'string' && typeof obj.__$schemaResolved === 'undefined') {
91
115
  results.push({
92
116
  ref: resolveReference(scope[scope.length - 1], obj.$schema),
@@ -95,25 +119,22 @@ export const collectReferences = (obj, results, scope, path) => {
95
119
  path: path.slice(0),
96
120
  });
97
121
  }
98
- let idx;
99
122
  if (Array.isArray(obj)) {
100
- idx = obj.length;
101
- while (idx--) {
102
- path.push(idx);
103
- collectReferences(obj[idx], results, scope, path);
123
+ for (let i = 0; i < obj.length; i++) {
124
+ path.push(i);
125
+ collectReferences(obj[i], results, scope, path, options, maxDepth, _depth + 1);
104
126
  path.pop();
105
127
  }
106
128
  }
107
129
  else {
108
130
  const keys = Object.keys(obj);
109
- idx = keys.length;
110
- while (idx--) {
131
+ for (const key of keys) {
111
132
  // do not recurse through resolved references and other z-schema props
112
- if (keys[idx].indexOf('__$') === 0 || doNotCollectReferencesFrom.includes(keys[idx])) {
133
+ if (isInternalKey(key) || NON_SCHEMA_KEYWORDS.includes(key)) {
113
134
  continue;
114
135
  }
115
- path.push(keys[idx]);
116
- collectReferences(obj[keys[idx]], results, scope, path);
136
+ path.push(key);
137
+ collectReferences(obj[key], results, scope, path, options, maxDepth, _depth + 1);
117
138
  path.pop();
118
139
  }
119
140
  }
@@ -151,29 +172,41 @@ const resolveReference = (base, ref) => {
151
172
  }
152
173
  return baseDir + ref;
153
174
  };
175
+ const isSimpleIdentifier = (id) => id[0] !== '#' && !id.includes('/') && !id.includes('.') && !id.includes('#');
154
176
  const resolveIdScope = (base, id) => {
155
177
  if (isAbsoluteUri(id)) {
156
178
  return id;
157
179
  }
158
180
  const baseStr = base ?? '';
159
181
  // Treat simple identifiers (no '/', '.', or '#') as same-document fragment ids
160
- if (id[0] !== '#' && !id.includes('/') && !id.includes('.') && !id.includes('#')) {
182
+ if (isSimpleIdentifier(id)) {
161
183
  const hashIndex = baseStr.indexOf('#');
162
184
  const baseNoFrag = hashIndex === -1 ? baseStr : baseStr.slice(0, hashIndex);
163
185
  return baseNoFrag + '#' + id;
164
186
  }
165
187
  return resolveReference(base, id);
166
188
  };
189
+ const resolveSchemaScopeId = (base, schema, id) => {
190
+ if (typeof schema.$id === 'string') {
191
+ return resolveReference(base, id);
192
+ }
193
+ return resolveIdScope(base, id);
194
+ };
167
195
  export class SchemaCompiler {
168
196
  validator;
169
197
  constructor(validator) {
170
198
  this.validator = validator;
171
199
  }
172
200
  collectAndCacheIds(schema) {
173
- const ids = collectIds(schema);
201
+ const ids = collectIds(schema, this.validator.options.maxRecursionDepth);
174
202
  for (const item of ids) {
175
203
  if (item.absoluteUri) {
176
204
  this.validator.scache.cacheSchemaByUri(item.absoluteUri, item.obj);
205
+ if (item.type === 'relative' && item.absoluteParent && isSimpleIdentifier(item.id)) {
206
+ const parentUri = item.absoluteParent.absoluteUri || item.absoluteParent.id;
207
+ const altAbsoluteUri = resolveReference(parentUri, item.id);
208
+ this.validator.scache.cacheSchemaByUri(altAbsoluteUri, item.obj);
209
+ }
177
210
  }
178
211
  else if (item.type === 'root') {
179
212
  this.validator.scache.cacheSchemaByUri(item.id, item.obj);
@@ -185,7 +218,7 @@ export class SchemaCompiler {
185
218
  // if schema is a string, assume it's a uri
186
219
  if (typeof schema === 'string') {
187
220
  const loadedSchema = this.validator.scache.getSchemaByUri(report, schema);
188
- if (!loadedSchema) {
221
+ if (typeof loadedSchema === 'undefined') {
189
222
  report.addError('SCHEMA_NOT_REACHABLE', [schema]);
190
223
  return false;
191
224
  }
@@ -232,21 +265,21 @@ export class SchemaCompiler {
232
265
  const isValidExceptReferences = report.isValid();
233
266
  delete schema.__$missingReferences;
234
267
  // collect all references that need to be resolved - $ref and $schema
235
- const refs = collectReferences(schema);
236
- let idx = refs.length;
237
- while (idx--) {
268
+ const useRefObjectScope = this.validator.options.version === 'draft2019-09' || this.validator.options.version === 'draft2020-12';
269
+ const refs = collectReferences(schema, undefined, undefined, undefined, { useRefObjectScope }, this.validator.options.maxRecursionDepth);
270
+ for (const refObj of refs) {
238
271
  // resolve all the collected references into __xxxResolved pointer
239
- const refObj = refs[idx];
240
272
  let response = this.validator.scache.getSchemaByUri(report, refObj.ref, schema);
241
273
  // we can try to use custom schemaReader if available
242
- if (!response) {
274
+ if (typeof response === 'undefined') {
243
275
  const schemaReader = getSchemaReader();
244
276
  if (schemaReader) {
245
277
  const remotePath = getRemotePath(refObj.ref);
246
278
  // it's supposed to return a valid schema
247
279
  const s = schemaReader(remotePath);
248
280
  if (s) {
249
- // it needs to have the id
281
+ // it needs to have the id (cast: schemaReader returns JsonSchema, but
282
+ // at this pre-compilation stage we treat it as an internal object)
250
283
  s.id = remotePath;
251
284
  // try to compile the schema
252
285
  const subreport = new Report(report);
@@ -260,7 +293,7 @@ export class SchemaCompiler {
260
293
  }
261
294
  }
262
295
  }
263
- if (!response) {
296
+ if (typeof response === 'undefined') {
264
297
  const hasNotValid = report.hasError('REMOTE_NOT_VALID', [refObj.ref]);
265
298
  const isAbsolute = isAbsoluteUri(refObj.ref);
266
299
  let isDownloaded = false;
@@ -314,23 +347,15 @@ export class SchemaCompiler {
314
347
  let compiled = 0, lastLoopCompiled;
315
348
  do {
316
349
  // remove all UNRESOLVABLE_REFERENCE errors before compiling array again
317
- let idx = report.errors.length;
318
- while (idx--) {
319
- if (report.errors[idx].code === 'UNRESOLVABLE_REFERENCE') {
320
- report.errors.splice(idx, 1);
321
- }
322
- }
350
+ report.errors = report.errors.filter((e) => e.code !== 'UNRESOLVABLE_REFERENCE');
323
351
  // remember how many were compiled in the last loop
324
352
  lastLoopCompiled = compiled;
325
353
  // count how many are compiled now
326
354
  compiled = this.compileArrayOfSchemasLoop(report, arr);
327
355
  // fix __$missingReferences if possible
328
- idx = arr.length;
329
- while (idx--) {
330
- const sch = arr[idx];
356
+ for (const sch of arr) {
331
357
  if (sch.__$missingReferences) {
332
- let idx2 = sch.__$missingReferences.length;
333
- while (idx2--) {
358
+ for (let idx2 = sch.__$missingReferences.length - 1; idx2 >= 0; idx2--) {
334
359
  const refObj = sch.__$missingReferences[idx2];
335
360
  const response = arr.find((x) => x.id === refObj.ref);
336
361
  if (response) {
@@ -350,11 +375,11 @@ export class SchemaCompiler {
350
375
  return report.isValid();
351
376
  }
352
377
  compileArrayOfSchemasLoop(mainReport, arr) {
353
- let idx = arr.length, compiledCount = 0;
354
- while (idx--) {
378
+ let compiledCount = 0;
379
+ for (const schema of arr) {
355
380
  // try to compile each schema separately
356
381
  const report = new Report(mainReport);
357
- const isValid = this.compileSchema(report, arr[idx]);
382
+ const isValid = this.compileSchema(report, schema);
358
383
  if (isValid) {
359
384
  compiledCount++;
360
385
  }