@warp-drive-mirror/json-api 5.9.0-alpha.17 → 5.9.0-alpha.19
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.
- package/declarations/-private/validator/utils.d.ts +37 -2
- package/dist/index.js +325 -69
- package/dist/unpkg/dev/index.js +325 -69
- package/dist/unpkg/dev-deprecated/index.js +325 -69
- package/package.json +4 -4
|
@@ -45,8 +45,43 @@ export declare class Reporter {
|
|
|
45
45
|
capabilities: CacheCapabilitiesManager;
|
|
46
46
|
contextDocument: StructuredDocument<ResourceDocument>;
|
|
47
47
|
errors: ErrorReport[];
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The maximum number of source lines the reporter will annotate and emit
|
|
50
|
+
* in a single `console.log` call when producing a report. Documents with
|
|
51
|
+
* more lines than this are chunked across multiple calls so that no
|
|
52
|
+
* single call ever has to spread an unbounded number of colorization
|
|
53
|
+
* args (which can exceed engine call-argument/stack limits for very
|
|
54
|
+
* large payloads).
|
|
55
|
+
*/
|
|
56
|
+
maxLines: number;
|
|
57
|
+
/**
|
|
58
|
+
* The number of source lines to show before/after each reported line when
|
|
59
|
+
* annotating the document. Stretches of source between annotated lines
|
|
60
|
+
* that are larger than this get collapsed into a single
|
|
61
|
+
* `... N lines skipped ...` marker rather than printed in full.
|
|
62
|
+
*/
|
|
63
|
+
contextLines: number;
|
|
64
|
+
/**
|
|
65
|
+
* The number of occurrences of an identical error message to annotate
|
|
66
|
+
* inline before collapsing the rest. When a message recurs beyond this
|
|
67
|
+
* count, the last shown occurrence's annotation is suffixed with
|
|
68
|
+
* `(recurs N more times)` and the remaining occurrences are omitted from
|
|
69
|
+
* the printed document entirely (though they still count toward the
|
|
70
|
+
* totals in the summary line).
|
|
71
|
+
*/
|
|
72
|
+
maxOccurrencesPerGroup: number;
|
|
73
|
+
/**
|
|
74
|
+
* The number of distinct error messages to annotate inline before
|
|
75
|
+
* refusing to show any more. Once this many distinct messages have been
|
|
76
|
+
* shown, further distinct messages are omitted entirely and rolled up
|
|
77
|
+
* into a single trailing `... and N more distinct issues ... not shown`
|
|
78
|
+
* line so nothing is dropped silently.
|
|
79
|
+
*/
|
|
80
|
+
maxDistinctIssues: number;
|
|
81
|
+
_ast: ReturnType<typeof jsonToAst> | undefined;
|
|
82
|
+
_jsonStr: string | undefined;
|
|
83
|
+
get jsonStr(): string;
|
|
84
|
+
get ast(): ReturnType<typeof jsonToAst>;
|
|
50
85
|
strict: {
|
|
51
86
|
linkage: boolean;
|
|
52
87
|
unknownType: boolean;
|
package/dist/index.js
CHANGED
|
@@ -60,7 +60,7 @@ function validateResourceFields(schema, resource, options) {
|
|
|
60
60
|
case 'hasMany':
|
|
61
61
|
{
|
|
62
62
|
if (field.options.linksMode) {
|
|
63
|
-
validateHasManyToLinksMode(resourceType, field);
|
|
63
|
+
validateHasManyToLinksMode(resourceType, field, relationshipDoc, options);
|
|
64
64
|
}
|
|
65
65
|
break;
|
|
66
66
|
}
|
|
@@ -71,31 +71,105 @@ function validateBelongsToLinksMode(resourceType, field, relationshipDoc, option
|
|
|
71
71
|
if (field.options.async) {
|
|
72
72
|
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but async is not yet supported`);
|
|
73
73
|
}
|
|
74
|
-
if (!
|
|
75
|
-
|
|
74
|
+
if (!field.options.async) {
|
|
75
|
+
const relationshipData = relationshipDoc.data;
|
|
76
|
+
if (Array.isArray(relationshipData)) {
|
|
77
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the relationship data for a belongsTo relationship is unexpectedly an array`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* If we are sync, we must have a related link when we have no related data field
|
|
82
|
+
*
|
|
83
|
+
* We explicitly allow `null`! Missing key or `undefined` are always invalid.
|
|
84
|
+
*/
|
|
85
|
+
if (relationshipData === undefined && !relationshipDoc.links?.related) {
|
|
86
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the relationship data is undefined and no link is present`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Nothing more to verify since we are empty
|
|
91
|
+
*/
|
|
92
|
+
if (!relationshipData) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* We are explicitly asked to not verify full-linkage
|
|
98
|
+
*/
|
|
99
|
+
if (!options.verifyIncluded) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* If we have a link, full-linkage verification is not required.
|
|
105
|
+
*/
|
|
106
|
+
if (relationshipDoc.links?.related) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* If we are sync and have relationship data, we must have full linkage to an included resource
|
|
112
|
+
*/
|
|
113
|
+
const includedDoc = options.included?.find(doc => doc.type === relationshipData.type && doc.id === relationshipData.id);
|
|
114
|
+
if (!includedDoc) {
|
|
115
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related data is not included`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
/**
|
|
119
|
+
* If we are async, we must have a related link.
|
|
120
|
+
*/
|
|
121
|
+
if (!relationshipDoc.links?.related) {
|
|
122
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related link is missing`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function validateHasManyToLinksMode(resourceType, field, relationshipDoc, options) {
|
|
127
|
+
if (field.options.async) {
|
|
128
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but async hasMany is not yet supported`);
|
|
76
129
|
}
|
|
77
130
|
const relationshipData = relationshipDoc.data;
|
|
78
|
-
if (Array.isArray(relationshipData)) {
|
|
79
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the relationship data for a
|
|
131
|
+
if (relationshipData !== undefined && !Array.isArray(relationshipData)) {
|
|
132
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the relationship data for a hasMany relationship is unexpectedly not an array`);
|
|
80
133
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* If we are sync, we must have a related link when we have no related data field
|
|
137
|
+
*
|
|
138
|
+
* We explicitly allow an empty array! Missing key or `undefined` are always invalid.
|
|
139
|
+
*/
|
|
140
|
+
if (relationshipData === undefined && !relationshipDoc.links?.related) {
|
|
141
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the relationship data is undefined and no link is present`);
|
|
84
142
|
}
|
|
85
|
-
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Nothing more to verify since we are empty
|
|
146
|
+
*/
|
|
147
|
+
if (!relationshipData || relationshipData.length === 0) {
|
|
86
148
|
return;
|
|
87
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* We are explicitly asked to not verify full-linkage
|
|
153
|
+
*/
|
|
88
154
|
if (!options.verifyIncluded) {
|
|
89
155
|
return;
|
|
90
156
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* If we have a link, full-linkage verification is not required.
|
|
160
|
+
*/
|
|
161
|
+
if (relationshipDoc.links?.related) {
|
|
162
|
+
return;
|
|
94
163
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* If we are sync and have relationship data, we must have full linkage to included resources
|
|
167
|
+
*/
|
|
168
|
+
for (const identifier of relationshipData) {
|
|
169
|
+
const includedDoc = options.included?.find(doc => doc.type === identifier.type && doc.id === identifier.id);
|
|
170
|
+
if (!includedDoc) {
|
|
171
|
+
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related data is not included`);
|
|
172
|
+
}
|
|
99
173
|
}
|
|
100
174
|
}
|
|
101
175
|
|
|
@@ -163,8 +237,60 @@ class Reporter {
|
|
|
163
237
|
capabilities;
|
|
164
238
|
contextDocument;
|
|
165
239
|
errors = [];
|
|
166
|
-
|
|
167
|
-
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The maximum number of source lines the reporter will annotate and emit
|
|
243
|
+
* in a single `console.log` call when producing a report. Documents with
|
|
244
|
+
* more lines than this are chunked across multiple calls so that no
|
|
245
|
+
* single call ever has to spread an unbounded number of colorization
|
|
246
|
+
* args (which can exceed engine call-argument/stack limits for very
|
|
247
|
+
* large payloads).
|
|
248
|
+
*/
|
|
249
|
+
maxLines = 500;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The number of source lines to show before/after each reported line when
|
|
253
|
+
* annotating the document. Stretches of source between annotated lines
|
|
254
|
+
* that are larger than this get collapsed into a single
|
|
255
|
+
* `... N lines skipped ...` marker rather than printed in full.
|
|
256
|
+
*/
|
|
257
|
+
contextLines = 2;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The number of occurrences of an identical error message to annotate
|
|
261
|
+
* inline before collapsing the rest. When a message recurs beyond this
|
|
262
|
+
* count, the last shown occurrence's annotation is suffixed with
|
|
263
|
+
* `(recurs N more times)` and the remaining occurrences are omitted from
|
|
264
|
+
* the printed document entirely (though they still count toward the
|
|
265
|
+
* totals in the summary line).
|
|
266
|
+
*/
|
|
267
|
+
maxOccurrencesPerGroup = 1;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The number of distinct error messages to annotate inline before
|
|
271
|
+
* refusing to show any more. Once this many distinct messages have been
|
|
272
|
+
* shown, further distinct messages are omitted entirely and rolled up
|
|
273
|
+
* into a single trailing `... and N more distinct issues ... not shown`
|
|
274
|
+
* line so nothing is dropped silently.
|
|
275
|
+
*/
|
|
276
|
+
maxDistinctIssues = 50;
|
|
277
|
+
// lazy: only parse the document into a string/AST if we actually need to
|
|
278
|
+
// locate an error, warning, or info within it. Clean documents (the vast
|
|
279
|
+
// majority) never pay this cost.
|
|
280
|
+
get jsonStr() {
|
|
281
|
+
if (this._jsonStr === undefined) {
|
|
282
|
+
this._jsonStr = JSON.stringify(this.contextDocument.content, null, 2);
|
|
283
|
+
}
|
|
284
|
+
return this._jsonStr;
|
|
285
|
+
}
|
|
286
|
+
get ast() {
|
|
287
|
+
if (!this._ast) {
|
|
288
|
+
this._ast = jsonToAst(this.jsonStr, {
|
|
289
|
+
loc: true
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return this._ast;
|
|
293
|
+
}
|
|
168
294
|
|
|
169
295
|
// TODO @runspired make this configurable to consuming apps before
|
|
170
296
|
// activating by default
|
|
@@ -213,10 +339,6 @@ class Reporter {
|
|
|
213
339
|
constructor(capabilities, doc) {
|
|
214
340
|
this.capabilities = capabilities;
|
|
215
341
|
this.contextDocument = doc;
|
|
216
|
-
this.jsonStr = JSON.stringify(doc.content, null, 2);
|
|
217
|
-
this.ast = jsonToAst(this.jsonStr, {
|
|
218
|
-
loc: true
|
|
219
|
-
});
|
|
220
342
|
}
|
|
221
343
|
searchTypes(type) {
|
|
222
344
|
if (!this._typeFilter) {
|
|
@@ -343,8 +465,6 @@ class Reporter {
|
|
|
343
465
|
return REGISTERED_EXTENSIONS.get(extensionName);
|
|
344
466
|
}
|
|
345
467
|
report(colorize = true) {
|
|
346
|
-
const lines = this.jsonStr.split('\n');
|
|
347
|
-
|
|
348
468
|
// sort the errors by line, then by column, then by type
|
|
349
469
|
const {
|
|
350
470
|
errors
|
|
@@ -352,59 +472,163 @@ class Reporter {
|
|
|
352
472
|
if (!errors.length) {
|
|
353
473
|
return;
|
|
354
474
|
}
|
|
475
|
+
const lines = this.jsonStr.split('\n');
|
|
355
476
|
errors.sort((a, b) => {
|
|
356
477
|
return a.loc.end.line < b.loc.end.line ? -1 : a.loc.end.column < b.loc.end.column ? -1 : compareType(a.type, b.type);
|
|
357
478
|
});
|
|
358
479
|
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
for (const error of errors) {
|
|
362
|
-
const line = error.loc.end.line;
|
|
363
|
-
if (!errorMap.has(line)) {
|
|
364
|
-
errorMap.set(line, []);
|
|
365
|
-
}
|
|
366
|
-
errorMap.get(line).push(error);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// splice the errors into the lines
|
|
370
|
-
const errorLines = [];
|
|
371
|
-
const colors = [];
|
|
480
|
+
// counts reflect every error/warning/info regardless of whether it ends
|
|
481
|
+
// up annotated inline below, so the header total is always accurate.
|
|
372
482
|
const counts = {
|
|
373
483
|
error: 0,
|
|
374
484
|
warning: 0,
|
|
375
485
|
info: 0
|
|
376
486
|
};
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
487
|
+
for (const error of errors) {
|
|
488
|
+
counts[error.type]++;
|
|
489
|
+
}
|
|
490
|
+
const contextStr = `${counts.error} errors and ${counts.warning} warnings found in the {json:api} document returned by ${this.contextDocument.request?.method ?? 'GET'} ${this.contextDocument.request?.url}`;
|
|
491
|
+
|
|
492
|
+
// group identical messages together so repeats can be collapsed down to
|
|
493
|
+
// a representative sample instead of printed in full.
|
|
494
|
+
const groups = new Map();
|
|
495
|
+
for (const error of errors) {
|
|
496
|
+
let group = groups.get(error.message);
|
|
497
|
+
if (!group) {
|
|
498
|
+
group = [];
|
|
499
|
+
groups.set(error.message, group);
|
|
500
|
+
}
|
|
501
|
+
group.push(error);
|
|
502
|
+
}
|
|
503
|
+
const activeErrors = new Set();
|
|
504
|
+
const recurrenceNoteFor = new Map();
|
|
505
|
+
let shownGroupCount = 0;
|
|
506
|
+
let hiddenGroupCount = 0;
|
|
507
|
+
let hiddenOccurrenceCount = 0;
|
|
508
|
+
for (const group of groups.values()) {
|
|
509
|
+
if (shownGroupCount < this.maxDistinctIssues) {
|
|
510
|
+
shownGroupCount++;
|
|
511
|
+
const showCount = Math.min(this.maxOccurrencesPerGroup, group.length);
|
|
512
|
+
for (let i = 0; i < showCount; i++) {
|
|
513
|
+
activeErrors.add(group[i]);
|
|
514
|
+
}
|
|
515
|
+
const extra = group.length - showCount;
|
|
516
|
+
if (extra > 0) {
|
|
517
|
+
recurrenceNoteFor.set(group[showCount - 1], extra);
|
|
400
518
|
}
|
|
519
|
+
} else {
|
|
520
|
+
hiddenGroupCount++;
|
|
521
|
+
hiddenOccurrenceCount += group.length;
|
|
401
522
|
}
|
|
402
523
|
}
|
|
403
|
-
const contextStr = `${counts.error} errors and ${counts.warning} warnings found in the {json:api} document returned by ${this.contextDocument.request?.method ?? 'GET'} ${this.contextDocument.request?.url}`;
|
|
404
|
-
const errorString = contextStr + `\n\n` + errorLines.join('\n');
|
|
405
524
|
|
|
406
|
-
//
|
|
407
|
-
|
|
525
|
+
// store the active (to-be-annotated) errors in a map by line
|
|
526
|
+
const errorMap = new Map();
|
|
527
|
+
for (const error of activeErrors) {
|
|
528
|
+
const line = error.loc.end.line;
|
|
529
|
+
let errorsForLine = errorMap.get(line);
|
|
530
|
+
if (!errorsForLine) {
|
|
531
|
+
errorsForLine = [];
|
|
532
|
+
errorMap.set(line, errorsForLine);
|
|
533
|
+
}
|
|
534
|
+
errorsForLine.push(error);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// determine which stretches of source to display: a window of
|
|
538
|
+
// `contextLines` around every active error line, merging windows that
|
|
539
|
+
// are close enough together that a skip marker wouldn't save anything.
|
|
540
|
+
const MERGE_GAP = 3;
|
|
541
|
+
const activeLines = Array.from(errorMap.keys()).sort((a, b) => a - b);
|
|
542
|
+
const ranges = [];
|
|
543
|
+
for (const line of activeLines) {
|
|
544
|
+
const start = Math.max(1, line - this.contextLines);
|
|
545
|
+
const end = Math.min(lines.length, line + this.contextLines);
|
|
546
|
+
const lastRange = ranges[ranges.length - 1];
|
|
547
|
+
if (lastRange && start <= lastRange[1] + MERGE_GAP + 1) {
|
|
548
|
+
lastRange[1] = Math.max(lastRange[1], end);
|
|
549
|
+
} else {
|
|
550
|
+
ranges.push([start, end]);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// extend the first/last range to the document boundary when the
|
|
555
|
+
// leading/trailing stretch is too small for a skip marker to be worth it
|
|
556
|
+
const firstRange = ranges[0];
|
|
557
|
+
if (firstRange && firstRange[0] - 1 <= MERGE_GAP + 1) {
|
|
558
|
+
firstRange[0] = 1;
|
|
559
|
+
}
|
|
560
|
+
const lastRangeOverall = ranges[ranges.length - 1];
|
|
561
|
+
if (lastRangeOverall && lines.length - lastRangeOverall[1] <= MERGE_GAP + 1) {
|
|
562
|
+
lastRangeOverall[1] = lines.length;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// render into chunks so that no single `console.log` call has to spread
|
|
566
|
+
// more than `this.maxLines` worth of rendered lines as colorization
|
|
567
|
+
// args.
|
|
568
|
+
const chunks = [{
|
|
569
|
+
text: [],
|
|
570
|
+
colors: []
|
|
571
|
+
}];
|
|
572
|
+
let renderedCount = 0;
|
|
573
|
+
const nextLine = (text, lineColors) => {
|
|
574
|
+
if (renderedCount > 0 && renderedCount % this.maxLines === 0) {
|
|
575
|
+
chunks.push({
|
|
576
|
+
text: [],
|
|
577
|
+
colors: []
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const chunk = chunks[chunks.length - 1];
|
|
581
|
+
chunk.text.push(text);
|
|
582
|
+
chunk.colors.push(...lineColors);
|
|
583
|
+
renderedCount++;
|
|
584
|
+
};
|
|
585
|
+
const pushSkipMarker = gap => {
|
|
586
|
+
nextLine(colorize ? `%c... ${gap} line${gap === 1 ? '' : 's'} skipped (no errors) ...%c` : `... ${gap} line${gap === 1 ? '' : 's'} skipped (no errors) ...`, ['color: grey; font-style: italic;', 'color: inherit; background-color: transparent;']);
|
|
587
|
+
};
|
|
588
|
+
const LINE_SIZE = String(lines.length).length;
|
|
589
|
+
for (let r = 0; r < ranges.length; r++) {
|
|
590
|
+
const [rangeStart, rangeEnd] = ranges[r];
|
|
591
|
+
if (r === 0) {
|
|
592
|
+
if (rangeStart > 1) {
|
|
593
|
+
pushSkipMarker(rangeStart - 1);
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
const gap = rangeStart - ranges[r - 1][1] - 1;
|
|
597
|
+
if (gap > 0) {
|
|
598
|
+
pushSkipMarker(gap);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
602
|
+
const line = lines[i - 1];
|
|
603
|
+
nextLine(colorize ? `${String(i).padEnd(LINE_SIZE, ' ')} \t%c${line}%c` : `${String(i).padEnd(LINE_SIZE, ' ')} \t${line}`, [`color: grey; background-color: transparent;`, `color: inherit; background-color: transparent;`]);
|
|
604
|
+
if (errorMap.has(i)) {
|
|
605
|
+
for (const error of errorMap.get(i)) {
|
|
606
|
+
const {
|
|
607
|
+
loc
|
|
608
|
+
} = error;
|
|
609
|
+
const start = loc.end.line === loc.start.line ? loc.start.column - 1 : loc.end.column - 1;
|
|
610
|
+
const end = loc.end.column - 1;
|
|
611
|
+
const symbol = error.type === 'error' ? '❌' : error.type === 'warning' ? '⚠️' : 'ℹ️';
|
|
612
|
+
const extra = recurrenceNoteFor.get(error);
|
|
613
|
+
const message = extra ? `${error.message} (recurs ${extra} more time${extra === 1 ? '' : 's'})` : error.message;
|
|
614
|
+
nextLine(colorize ? `${''.padStart(LINE_SIZE, ' ') + symbol}\t${' '.repeat(start)}%c^${'~'.repeat(end - start)} %c//%c ${message}%c` : `${''.padStart(LINE_SIZE, ' ') + symbol}\t${' '.repeat(start)}^${'~'.repeat(end - start)} // ${message}`, [error.type === 'error' ? 'color: red;' : error.type === 'warning' ? 'color: orange;' : 'color: blue;', 'color: grey;', error.type === 'error' ? 'color: red;' : error.type === 'warning' ? 'color: orange;' : 'color: blue;', 'color: inherit; background-color: transparent;']);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const lastRenderedRange = ranges[ranges.length - 1];
|
|
620
|
+
if (lastRenderedRange && lastRenderedRange[1] < lines.length) {
|
|
621
|
+
pushSkipMarker(lines.length - lastRenderedRange[1]);
|
|
622
|
+
}
|
|
623
|
+
if (hiddenGroupCount > 0) {
|
|
624
|
+
nextLine(colorize ? `%c... and ${hiddenGroupCount} more distinct issue${hiddenGroupCount === 1 ? '' : 's'} (${hiddenOccurrenceCount} occurrence${hiddenOccurrenceCount === 1 ? '' : 's'}) not shown ...%c` : `... and ${hiddenGroupCount} more distinct issues (${hiddenOccurrenceCount} occurrences) not shown ...`, ['color: grey; font-style: italic;', 'color: inherit; background-color: transparent;']);
|
|
625
|
+
}
|
|
626
|
+
chunks.forEach((chunk, index) => {
|
|
627
|
+
const prefix = index === 0 ? `${contextStr}\n\n` : '';
|
|
628
|
+
const chunkString = prefix + chunk.text.join('\n');
|
|
629
|
+
// eslint-disable-next-line no-console, @typescript-eslint/no-unused-expressions
|
|
630
|
+
colorize ? console.log(chunkString, ...chunk.colors) : console.log(chunkString);
|
|
631
|
+
});
|
|
408
632
|
if (macroCondition(getGlobalConfig().WarpDriveMirror.features.JSON_API_CACHE_VALIDATION_ERRORS)) {
|
|
409
633
|
if (counts.error > 0) {
|
|
410
634
|
throw new Error(contextStr);
|
|
@@ -465,6 +689,27 @@ function getRemoteField(fields, key) {
|
|
|
465
689
|
}
|
|
466
690
|
return field;
|
|
467
691
|
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Detects the common mistake of providing a field's `name` in a payload
|
|
695
|
+
* when the field's schema defines a `sourceKey` that should be used instead.
|
|
696
|
+
*
|
|
697
|
+
* @internal
|
|
698
|
+
*/
|
|
699
|
+
function getSourceKeyMismatch(fields, key) {
|
|
700
|
+
const field = getRemoteField(fields, key);
|
|
701
|
+
if (!field) {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
const sourceKey = 'sourceKey' in field ? field.sourceKey : undefined;
|
|
705
|
+
if (sourceKey && sourceKey !== key) {
|
|
706
|
+
return {
|
|
707
|
+
field,
|
|
708
|
+
sourceKey
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
468
713
|
function addResourceToMap(map, resource, index, location) {
|
|
469
714
|
if (!map.has(resource.type)) {
|
|
470
715
|
map.set(resource.type, new Map());
|
|
@@ -856,7 +1101,11 @@ function validateResourceAttributes(reporter, type, resource, path) {
|
|
|
856
1101
|
if (!field && actualField) {
|
|
857
1102
|
reporter.warn([...path, key], `Expected the ${actualField.kind} field "${key}" to not have its own data in the ResourceObject's attributes. Likely this field should either not be returned in this payload or the field definition should be updated in the schema.`);
|
|
858
1103
|
} else if (!field) {
|
|
859
|
-
|
|
1104
|
+
const sourceKeyMismatch = getSourceKeyMismatch(fields, key);
|
|
1105
|
+
if (sourceKeyMismatch) {
|
|
1106
|
+
const method = reporter.strict.unknownAttribute ? 'error' : 'warn';
|
|
1107
|
+
reporter[method]([...path, key], `Expected the "${sourceKeyMismatch.field.kind}" field "${key}" to be provided using its sourceKey "${sourceKeyMismatch.sourceKey}" instead of its field name "${key}". Update the payload to use "${sourceKeyMismatch.sourceKey}" as the key, or remove the sourceKey from the field's definition in the ResourceSchema for "${type}" if the field name should be used instead.`);
|
|
1108
|
+
} else if (key.includes(':')) {
|
|
860
1109
|
const extensionName = key.split(':')[0];
|
|
861
1110
|
if (reporter.hasExtension(extensionName)) {
|
|
862
1111
|
const extension = reporter.getExtension(extensionName);
|
|
@@ -881,16 +1130,23 @@ function validateResourceAttributes(reporter, type, resource, path) {
|
|
|
881
1130
|
// TODO @runspired we should validate that field values are valid JSON and not instances
|
|
882
1131
|
}
|
|
883
1132
|
function validateResourceRelationships(reporter, type, resource, path) {
|
|
884
|
-
const
|
|
1133
|
+
const fields = reporter.schema.fields({
|
|
885
1134
|
type
|
|
886
1135
|
});
|
|
1136
|
+
const cacheFields = reporter.schema.cacheFields?.({
|
|
1137
|
+
type
|
|
1138
|
+
}) ?? fields;
|
|
887
1139
|
for (const [key] of Object.entries(resource)) {
|
|
888
|
-
const field = getRemoteField(
|
|
889
|
-
const actualField =
|
|
1140
|
+
const field = getRemoteField(cacheFields, key);
|
|
1141
|
+
const actualField = cacheFields.get(key);
|
|
890
1142
|
if (!field && actualField) {
|
|
891
1143
|
reporter.warn([...path, key], `Expected the ${actualField.kind} field "${key}" to not have its own data in the ResourceObject's relationships. Likely this field should either not be returned in this payload or the field definition should be updated in the schema.`);
|
|
892
1144
|
} else if (!field) {
|
|
893
|
-
|
|
1145
|
+
const sourceKeyMismatch = getSourceKeyMismatch(fields, key);
|
|
1146
|
+
if (sourceKeyMismatch) {
|
|
1147
|
+
const method = reporter.strict.unknownRelationship ? 'error' : 'warn';
|
|
1148
|
+
reporter[method]([...path, key], `Expected the "${sourceKeyMismatch.field.kind}" field "${key}" to be provided using its sourceKey "${sourceKeyMismatch.sourceKey}" instead of its field name "${key}". Update the payload to use "${sourceKeyMismatch.sourceKey}" as the key, or remove the sourceKey from the field's definition in the ResourceSchema for "${type}" if the field name should be used instead.`);
|
|
1149
|
+
} else if (key.includes(':')) {
|
|
894
1150
|
const extensionName = key.split(':')[0];
|
|
895
1151
|
if (reporter.hasExtension(extensionName)) {
|
|
896
1152
|
const extension = reporter.getExtension(extensionName);
|