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