@warp-drive-mirror/json-api 5.8.0-alpha.37 → 5.8.0-alpha.40
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/dist/unpkg/dev/index.js +252 -139
- package/dist/unpkg/dev-deprecated/index.js +246 -172
- package/dist/unpkg/prod/index.js +4 -1357
- package/dist/unpkg/prod-deprecated/index.js +4 -1357
- package/package.json +12 -20
- package/dist/unpkg/dev/declarations/-private/cache.d.ts +0 -445
- package/dist/unpkg/dev/declarations/-private/validate-document-fields.d.ts +0 -3
- package/dist/unpkg/dev/declarations/-private/validator/1.1/7.1_top-level-document-members.d.ts +0 -1
- package/dist/unpkg/dev/declarations/-private/validator/1.1/7.2_resource-objects.d.ts +0 -1
- package/dist/unpkg/dev/declarations/-private/validator/1.1/links.d.ts +0 -1
- package/dist/unpkg/dev/declarations/-private/validator/index.d.ts +0 -4
- package/dist/unpkg/dev/declarations/-private/validator/utils.d.ts +0 -75
- package/dist/unpkg/dev/declarations/index.d.ts +0 -5
- package/dist/unpkg/dev-deprecated/declarations/-private/cache.d.ts +0 -445
- package/dist/unpkg/dev-deprecated/declarations/-private/validate-document-fields.d.ts +0 -3
- package/dist/unpkg/dev-deprecated/declarations/-private/validator/1.1/7.1_top-level-document-members.d.ts +0 -1
- package/dist/unpkg/dev-deprecated/declarations/-private/validator/1.1/7.2_resource-objects.d.ts +0 -1
- package/dist/unpkg/dev-deprecated/declarations/-private/validator/1.1/links.d.ts +0 -1
- package/dist/unpkg/dev-deprecated/declarations/-private/validator/index.d.ts +0 -4
- package/dist/unpkg/dev-deprecated/declarations/-private/validator/utils.d.ts +0 -75
- package/dist/unpkg/dev-deprecated/declarations/index.d.ts +0 -5
- package/dist/unpkg/prod/declarations/-private/cache.d.ts +0 -445
- package/dist/unpkg/prod/declarations/-private/validate-document-fields.d.ts +0 -3
- package/dist/unpkg/prod/declarations/-private/validator/1.1/7.1_top-level-document-members.d.ts +0 -1
- package/dist/unpkg/prod/declarations/-private/validator/1.1/7.2_resource-objects.d.ts +0 -1
- package/dist/unpkg/prod/declarations/-private/validator/1.1/links.d.ts +0 -1
- package/dist/unpkg/prod/declarations/-private/validator/index.d.ts +0 -4
- package/dist/unpkg/prod/declarations/-private/validator/utils.d.ts +0 -75
- package/dist/unpkg/prod/declarations/index.d.ts +0 -5
- package/dist/unpkg/prod-deprecated/declarations/-private/cache.d.ts +0 -445
- package/dist/unpkg/prod-deprecated/declarations/-private/validate-document-fields.d.ts +0 -3
- package/dist/unpkg/prod-deprecated/declarations/-private/validator/1.1/7.1_top-level-document-members.d.ts +0 -1
- package/dist/unpkg/prod-deprecated/declarations/-private/validator/1.1/7.2_resource-objects.d.ts +0 -1
- package/dist/unpkg/prod-deprecated/declarations/-private/validator/1.1/links.d.ts +0 -1
- package/dist/unpkg/prod-deprecated/declarations/-private/validator/index.d.ts +0 -4
- package/dist/unpkg/prod-deprecated/declarations/-private/validator/utils.d.ts +0 -75
- package/dist/unpkg/prod-deprecated/declarations/index.d.ts +0 -5
package/dist/unpkg/prod/index.js
CHANGED
|
@@ -1,934 +1,14 @@
|
|
|
1
1
|
import { graphFor, peekGraph, isBelongsTo } from '@warp-drive-mirror/core/graph/-private';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { macroCondition, getGlobalConfig } from '@embroider/macros';
|
|
2
|
+
import { assertPrivateCapabilities, isResourceKey, isRequestKey } from '@warp-drive-mirror/core/store/-private';
|
|
3
|
+
import 'fuse.js';
|
|
4
|
+
import 'json-to-ast';
|
|
6
5
|
|
|
7
|
-
function validateDocumentFields(schema, jsonApiDoc) {
|
|
8
|
-
const {
|
|
9
|
-
data,
|
|
10
|
-
included
|
|
11
|
-
} = jsonApiDoc;
|
|
12
|
-
if (data === null) {
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
if (typeof jsonApiDoc.data !== 'object') {
|
|
16
|
-
throw new Error(`Expected a resource object in the 'data' property in the document provided to the cache, but was ${typeof jsonApiDoc.data}`);
|
|
17
|
-
}
|
|
18
|
-
if (Array.isArray(data)) {
|
|
19
|
-
for (const resource of data) {
|
|
20
|
-
validateResourceFields(schema, resource, {
|
|
21
|
-
verifyIncluded: true,
|
|
22
|
-
included
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
} else {
|
|
26
|
-
validateResourceFields(schema, data, {
|
|
27
|
-
verifyIncluded: true,
|
|
28
|
-
included
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (included) {
|
|
32
|
-
for (const resource of included) {
|
|
33
|
-
validateResourceFields(schema, resource, {
|
|
34
|
-
verifyIncluded: false
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function validateResourceFields(schema, resource, options) {
|
|
40
|
-
if (!resource.relationships) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
const resourceType = resource.type;
|
|
44
|
-
const fields = schema.fields({
|
|
45
|
-
type: resource.type
|
|
46
|
-
});
|
|
47
|
-
for (const [type, relationshipDoc] of Object.entries(resource.relationships)) {
|
|
48
|
-
const field = fields.get(type);
|
|
49
|
-
if (!field) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
switch (field.kind) {
|
|
53
|
-
case 'belongsTo':
|
|
54
|
-
{
|
|
55
|
-
if (field.options.linksMode) {
|
|
56
|
-
validateBelongsToLinksMode(resourceType, field, relationshipDoc, options);
|
|
57
|
-
}
|
|
58
|
-
break;
|
|
59
|
-
}
|
|
60
|
-
case 'hasMany':
|
|
61
|
-
{
|
|
62
|
-
if (field.options.linksMode) {
|
|
63
|
-
validateHasManyToLinksMode(resourceType, field);
|
|
64
|
-
}
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function validateBelongsToLinksMode(resourceType, field, relationshipDoc, options) {
|
|
71
|
-
if (field.options.async) {
|
|
72
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but async is not yet supported`);
|
|
73
|
-
}
|
|
74
|
-
if (!relationshipDoc.links?.related) {
|
|
75
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related link is missing`);
|
|
76
|
-
}
|
|
77
|
-
const relationshipData = relationshipDoc.data;
|
|
78
|
-
if (Array.isArray(relationshipData)) {
|
|
79
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the relationship data for a belongsTo relationship is unexpectedly an array`);
|
|
80
|
-
}
|
|
81
|
-
// Explicitly allow `null`! Missing key or `undefined` are always invalid.
|
|
82
|
-
if (relationshipData === undefined) {
|
|
83
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the relationship data is undefined`);
|
|
84
|
-
}
|
|
85
|
-
if (relationshipData === null) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (!options.verifyIncluded) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const includedDoc = options.included?.find(doc => doc.type === relationshipData.type && doc.id === relationshipData.id);
|
|
92
|
-
if (!includedDoc) {
|
|
93
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but the related data is not included`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
function validateHasManyToLinksMode(resourceType, field, _relationshipDoc, _options) {
|
|
97
|
-
if (field.options.async) {
|
|
98
|
-
throw new Error(`Cannot fetch ${resourceType}.${field.name} because the field is in linksMode but async hasMany is not yet supported`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function inspectType(obj) {
|
|
103
|
-
if (obj === null) {
|
|
104
|
-
return 'null';
|
|
105
|
-
}
|
|
106
|
-
if (Array.isArray(obj)) {
|
|
107
|
-
return 'array';
|
|
108
|
-
}
|
|
109
|
-
if (typeof obj === 'object') {
|
|
110
|
-
const proto = Object.getPrototypeOf(obj);
|
|
111
|
-
if (proto === null) {
|
|
112
|
-
return 'object';
|
|
113
|
-
}
|
|
114
|
-
if (proto === Object.prototype) {
|
|
115
|
-
return 'object';
|
|
116
|
-
}
|
|
117
|
-
return `object (${proto.constructor?.name})`;
|
|
118
|
-
}
|
|
119
|
-
if (typeof obj === 'function') {
|
|
120
|
-
return 'function';
|
|
121
|
-
}
|
|
122
|
-
if (typeof obj === 'string') {
|
|
123
|
-
return 'string';
|
|
124
|
-
}
|
|
125
|
-
if (typeof obj === 'number') {
|
|
126
|
-
return 'number';
|
|
127
|
-
}
|
|
128
|
-
if (typeof obj === 'boolean') {
|
|
129
|
-
return 'boolean';
|
|
130
|
-
}
|
|
131
|
-
if (typeof obj === 'symbol') {
|
|
132
|
-
return 'symbol';
|
|
133
|
-
}
|
|
134
|
-
if (typeof obj === 'bigint') {
|
|
135
|
-
return 'bigint';
|
|
136
|
-
}
|
|
137
|
-
if (typeof obj === 'undefined') {
|
|
138
|
-
return 'undefined';
|
|
139
|
-
}
|
|
140
|
-
return 'unknown';
|
|
141
|
-
}
|
|
142
|
-
function isSimpleObject(obj) {
|
|
143
|
-
if (obj === null) {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
if (Array.isArray(obj)) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
if (typeof obj !== 'object') {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
const proto = Object.getPrototypeOf(obj);
|
|
153
|
-
if (proto === null) {
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
if (proto === Object.prototype) {
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
return false;
|
|
160
|
-
}
|
|
161
|
-
const RELATIONSHIP_FIELD_KINDS = ['belongsTo', 'hasMany', 'resource', 'collection'];
|
|
162
|
-
class Reporter {
|
|
163
|
-
capabilities;
|
|
164
|
-
contextDocument;
|
|
165
|
-
errors = [];
|
|
166
|
-
ast;
|
|
167
|
-
jsonStr;
|
|
168
|
-
|
|
169
|
-
// TODO @runspired make this configurable to consuming apps before
|
|
170
|
-
// activating by default
|
|
171
|
-
strict = {
|
|
172
|
-
linkage: true,
|
|
173
|
-
unknownType: true,
|
|
174
|
-
unknownAttribute: true,
|
|
175
|
-
unknownRelationship: true
|
|
176
|
-
};
|
|
177
|
-
constructor(capabilities, doc) {
|
|
178
|
-
this.capabilities = capabilities;
|
|
179
|
-
this.contextDocument = doc;
|
|
180
|
-
this.jsonStr = JSON.stringify(doc.content, null, 2);
|
|
181
|
-
this.ast = jsonToAst(this.jsonStr, {
|
|
182
|
-
loc: true
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
searchTypes(type) {
|
|
186
|
-
if (!this._typeFilter) {
|
|
187
|
-
const allTypes = this.schema.resourceTypes();
|
|
188
|
-
this._typeFilter = new Fuse(allTypes);
|
|
189
|
-
}
|
|
190
|
-
const result = this._typeFilter.search(type);
|
|
191
|
-
return result;
|
|
192
|
-
}
|
|
193
|
-
_fieldFilters = new Map();
|
|
194
|
-
searchFields(type, field) {
|
|
195
|
-
if (!this._fieldFilters.has(type)) {
|
|
196
|
-
const allFields = this.schema.fields({
|
|
197
|
-
type
|
|
198
|
-
});
|
|
199
|
-
const allCacheFields = this.schema.cacheFields?.({
|
|
200
|
-
type
|
|
201
|
-
}) ?? allFields;
|
|
202
|
-
const attrs = Array.from(allCacheFields.values()).filter(isRemoteField).map(v => v.name);
|
|
203
|
-
this._fieldFilters.set(type, new Fuse(attrs));
|
|
204
|
-
}
|
|
205
|
-
const result = this._fieldFilters.get(type).search(field);
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
get schema() {
|
|
209
|
-
return this.capabilities.schema;
|
|
210
|
-
}
|
|
211
|
-
getLocation(path, kind) {
|
|
212
|
-
if (path.length === 0) {
|
|
213
|
-
return this.ast.loc;
|
|
214
|
-
}
|
|
215
|
-
let priorNode = this.ast;
|
|
216
|
-
let node = this.ast;
|
|
217
|
-
for (const segment of path) {
|
|
218
|
-
//
|
|
219
|
-
// handle array paths
|
|
220
|
-
//
|
|
221
|
-
if (typeof segment === 'number') {
|
|
222
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
223
|
-
if (!test) {
|
|
224
|
-
throw new Error(`Because the segment is a number, expected a node of type Array`);
|
|
225
|
-
}
|
|
226
|
-
})(node.type === 'Array') : {};
|
|
227
|
-
if (node.children && node.children[segment]) {
|
|
228
|
-
priorNode = node;
|
|
229
|
-
const childNode = node.children[segment];
|
|
230
|
-
if (childNode.type === 'Object' || childNode.type === 'Array') {
|
|
231
|
-
node = childNode;
|
|
232
|
-
} else {
|
|
233
|
-
// set to the closest node we can find
|
|
234
|
-
return node.loc;
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
// set to the closest node we can find
|
|
238
|
-
// as we had no children
|
|
239
|
-
return priorNode.loc;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
// handle object paths
|
|
244
|
-
//
|
|
245
|
-
} else {
|
|
246
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
247
|
-
if (!test) {
|
|
248
|
-
throw new Error(`Because the segment is a string, expected a node of type Object`);
|
|
249
|
-
}
|
|
250
|
-
})(node.type === 'Object') : {};
|
|
251
|
-
const child = node.children.find(childCandidate => {
|
|
252
|
-
if (childCandidate.type === 'Property') {
|
|
253
|
-
return childCandidate.key.type === 'Identifier' && childCandidate.key.value === segment;
|
|
254
|
-
}
|
|
255
|
-
return false;
|
|
256
|
-
});
|
|
257
|
-
if (child) {
|
|
258
|
-
if (child.value.type === 'Object' || child.value.type === 'Array') {
|
|
259
|
-
priorNode = node;
|
|
260
|
-
node = child.value;
|
|
261
|
-
} else {
|
|
262
|
-
// set to the closest node we can find
|
|
263
|
-
return kind === 'key' ? child.key.loc : child.value.loc;
|
|
264
|
-
}
|
|
265
|
-
} else {
|
|
266
|
-
// set to the closest node we can find
|
|
267
|
-
return priorNode.loc;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return node.loc;
|
|
272
|
-
}
|
|
273
|
-
error(path, message, kind = 'key') {
|
|
274
|
-
const loc = this.getLocation(path, kind);
|
|
275
|
-
this.errors.push({
|
|
276
|
-
path,
|
|
277
|
-
message,
|
|
278
|
-
loc,
|
|
279
|
-
type: 'error',
|
|
280
|
-
kind
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
warn(path, message, kind = 'key') {
|
|
284
|
-
const loc = this.getLocation(path, kind);
|
|
285
|
-
this.errors.push({
|
|
286
|
-
path,
|
|
287
|
-
message,
|
|
288
|
-
loc,
|
|
289
|
-
type: 'warning',
|
|
290
|
-
kind
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
info(path, message, kind = 'key') {
|
|
294
|
-
const loc = this.getLocation(path, kind);
|
|
295
|
-
this.errors.push({
|
|
296
|
-
path,
|
|
297
|
-
message,
|
|
298
|
-
loc,
|
|
299
|
-
type: 'info',
|
|
300
|
-
kind
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
hasExtension(extensionName) {
|
|
304
|
-
return REGISTERED_EXTENSIONS.has(extensionName);
|
|
305
|
-
}
|
|
306
|
-
getExtension(extensionName) {
|
|
307
|
-
return REGISTERED_EXTENSIONS.get(extensionName);
|
|
308
|
-
}
|
|
309
|
-
report(colorize = true) {
|
|
310
|
-
const lines = this.jsonStr.split('\n');
|
|
311
|
-
|
|
312
|
-
// sort the errors by line, then by column, then by type
|
|
313
|
-
const {
|
|
314
|
-
errors
|
|
315
|
-
} = this;
|
|
316
|
-
if (!errors.length) {
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
errors.sort((a, b) => {
|
|
320
|
-
return a.loc.end.line < b.loc.end.line ? -1 : a.loc.end.column < b.loc.end.column ? -1 : compareType(a.type, b.type);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// store the errors in a map by line
|
|
324
|
-
const errorMap = new Map();
|
|
325
|
-
for (const error of errors) {
|
|
326
|
-
const line = error.loc.end.line;
|
|
327
|
-
if (!errorMap.has(line)) {
|
|
328
|
-
errorMap.set(line, []);
|
|
329
|
-
}
|
|
330
|
-
errorMap.get(line).push(error);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// splice the errors into the lines
|
|
334
|
-
const errorLines = [];
|
|
335
|
-
const colors = [];
|
|
336
|
-
const counts = {
|
|
337
|
-
error: 0,
|
|
338
|
-
warning: 0,
|
|
339
|
-
info: 0
|
|
340
|
-
};
|
|
341
|
-
const LINE_SIZE = String(lines.length).length;
|
|
342
|
-
for (let i = 0; i < lines.length; i++) {
|
|
343
|
-
const line = lines[i];
|
|
344
|
-
errorLines.push(colorize ? `${String(i + 1).padEnd(LINE_SIZE, ' ')} \t%c${line}%c` : `${String(i + 1).padEnd(LINE_SIZE, ' ')} \t${line}`);
|
|
345
|
-
colors.push(`color: grey; background-color: transparent;`,
|
|
346
|
-
// first color sets color
|
|
347
|
-
`color: inherit; background-color: transparent;` // second color resets the color profile
|
|
348
|
-
);
|
|
349
|
-
if (errorMap.has(i + 1)) {
|
|
350
|
-
const errorsForLine = errorMap.get(i + 1);
|
|
351
|
-
for (const error of errorsForLine) {
|
|
352
|
-
counts[error.type]++;
|
|
353
|
-
const {
|
|
354
|
-
loc,
|
|
355
|
-
message
|
|
356
|
-
} = error;
|
|
357
|
-
const start = loc.end.line === loc.start.line ? loc.start.column - 1 : loc.end.column - 1;
|
|
358
|
-
const end = loc.end.column - 1;
|
|
359
|
-
const symbol = error.type === 'error' ? '❌' : error.type === 'warning' ? '⚠️' : 'ℹ️';
|
|
360
|
-
const errorLine = 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}`;
|
|
361
|
-
errorLines.push(errorLine);
|
|
362
|
-
colors.push(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;' // reset color
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const contextStr = `${counts.error} errors and ${counts.warning} warnings found in the {json:api} document returned by ${this.contextDocument.request?.method} ${this.contextDocument.request?.url}`;
|
|
368
|
-
const errorString = contextStr + `\n\n` + errorLines.join('\n');
|
|
369
|
-
|
|
370
|
-
// eslint-disable-next-line no-console, @typescript-eslint/no-unused-expressions
|
|
371
|
-
colorize ? console.log(errorString, ...colors) : console.log(errorString);
|
|
372
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.features.JSON_API_CACHE_VALIDATION_ERRORS)) {
|
|
373
|
-
if (counts.error > 0) {
|
|
374
|
-
throw new Error(contextStr);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// we always want to sort errors first, then warnings, then info
|
|
381
|
-
function compareType(a, b) {
|
|
382
|
-
if (a === b) {
|
|
383
|
-
return 0;
|
|
384
|
-
}
|
|
385
|
-
if (a === 'error') {
|
|
386
|
-
return -1;
|
|
387
|
-
}
|
|
388
|
-
if (b === 'error') {
|
|
389
|
-
return 1;
|
|
390
|
-
}
|
|
391
|
-
if (a === 'warning') {
|
|
392
|
-
return -1;
|
|
393
|
-
}
|
|
394
|
-
if (b === 'warning') {
|
|
395
|
-
return 1;
|
|
396
|
-
}
|
|
397
|
-
return 0;
|
|
398
|
-
}
|
|
399
|
-
const REGISTERED_EXTENSIONS = new Map();
|
|
400
6
|
function isMetaDocument(doc) {
|
|
401
7
|
return !(doc instanceof Error) && doc.content && !('data' in doc.content) && !('included' in doc.content) && 'meta' in doc.content;
|
|
402
8
|
}
|
|
403
9
|
function isErrorDocument(doc) {
|
|
404
10
|
return doc instanceof Error;
|
|
405
11
|
}
|
|
406
|
-
function isPushedDocument(doc) {
|
|
407
|
-
return !!doc && typeof doc === 'object' && 'content' in doc && !('request' in doc) && !('response' in doc);
|
|
408
|
-
}
|
|
409
|
-
function logPotentialMatches(matches, kind) {
|
|
410
|
-
if (matches.length === 0) {
|
|
411
|
-
return '';
|
|
412
|
-
}
|
|
413
|
-
if (matches.length === 1) {
|
|
414
|
-
return ` Did you mean this available ${kind} "${matches[0].item}"?`;
|
|
415
|
-
}
|
|
416
|
-
const potentialMatches = matches.map(match => match.item).join('", "');
|
|
417
|
-
return ` Did you mean one of these available ${kind}s: "${potentialMatches}"?`;
|
|
418
|
-
}
|
|
419
|
-
function isRemoteField(v) {
|
|
420
|
-
return !(v.kind === '@local' || v.kind === 'alias' || v.kind === 'derived');
|
|
421
|
-
}
|
|
422
|
-
function getRemoteField(fields, key) {
|
|
423
|
-
const field = fields.get(key);
|
|
424
|
-
if (!field) {
|
|
425
|
-
return undefined;
|
|
426
|
-
}
|
|
427
|
-
if (!isRemoteField(field)) {
|
|
428
|
-
return undefined;
|
|
429
|
-
}
|
|
430
|
-
return field;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const VALID_TOP_LEVEL_MEMBERS = ['data', 'included', 'meta', 'jsonapi', 'links'];
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Reports issues which violate the JSON:API spec for top-level members.
|
|
437
|
-
*
|
|
438
|
-
* Version: 1.1
|
|
439
|
-
* Section: 7.1
|
|
440
|
-
* Link: https://jsonapi.org/format/#document-top-level
|
|
441
|
-
*
|
|
442
|
-
* @internal
|
|
443
|
-
*/
|
|
444
|
-
function validateTopLevelDocumentMembers(reporter, doc) {
|
|
445
|
-
const keys = Object.keys(doc);
|
|
446
|
-
for (const key of keys) {
|
|
447
|
-
if (!VALID_TOP_LEVEL_MEMBERS.includes(key)) {
|
|
448
|
-
if (key.includes(':')) {
|
|
449
|
-
// TODO @runspired expose the API to enable folks to add validation for their own extensions
|
|
450
|
-
const extensionName = key.split(':')[0];
|
|
451
|
-
if (reporter.hasExtension(extensionName)) {
|
|
452
|
-
const extension = reporter.getExtension(extensionName);
|
|
453
|
-
extension(reporter, [key]);
|
|
454
|
-
} else {
|
|
455
|
-
reporter.warn([key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} member`);
|
|
456
|
-
}
|
|
457
|
-
} else {
|
|
458
|
-
reporter.error([key], `Unrecognized top-level member. The data it provides is ignored as it is not a valid {json:api} member`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// additional rules for top-level members
|
|
464
|
-
// ======================================
|
|
465
|
-
|
|
466
|
-
// 1. MUST have either `data`, `errors`, or `meta`
|
|
467
|
-
if (!('data' in doc) && !('errors' in doc) && !('meta' in doc)) {
|
|
468
|
-
reporter.error([], 'A {json:api} Document must contain one-of `data` `errors` or `meta`');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// 2. MUST NOT have both `data` and `errors`
|
|
472
|
-
if ('data' in doc && 'errors' in doc) {
|
|
473
|
-
reporter.error(['data'], 'A {json:api} Document MUST NOT contain both `data` and `errors` members');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// 3. MUST NOT have both `included` and `errors`
|
|
477
|
-
// while not explicitly stated in the spec, this is a logical extension of the above rule
|
|
478
|
-
// since `included` is only valid when `data` is present.
|
|
479
|
-
if ('included' in doc && 'errors' in doc) {
|
|
480
|
-
reporter.error(['included'], 'A {json:api} Document MUST NOT contain both `included` and `errors` members');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// 4. MUST NOT have `included` if `data` is not present
|
|
484
|
-
if ('included' in doc && !('data' in doc)) {
|
|
485
|
-
reporter.error(['included'], 'A {json:api} Document MUST NOT contain `included` if `data` is not present');
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 5. MUST NOT have `included` if `data` is null
|
|
489
|
-
// when strictly enforcing full linkage, we need to ensure that `included` is not present if `data` is null
|
|
490
|
-
// however, most APIs will ignore this rule for DELETE requests, so unless strict linkage is enabled, we will only warn
|
|
491
|
-
// about this issue.
|
|
492
|
-
if ('included' in doc && doc.data === null) {
|
|
493
|
-
const isMaybeDelete = reporter.contextDocument.request?.method?.toUpperCase() === 'DELETE' || reporter.contextDocument.request?.op === 'deleteRecord';
|
|
494
|
-
const method = !reporter.strict.linkage && isMaybeDelete ? 'warn' : 'error';
|
|
495
|
-
reporter[method](['included'], 'A {json:api} Document MUST NOT contain `included` if `data` is null');
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Simple Validation of Top-Level Members
|
|
499
|
-
// ==========================================
|
|
500
|
-
// 1. `data` MUST be a single resource object or an array of resource objects or `null`
|
|
501
|
-
if ('data' in doc) {
|
|
502
|
-
const dataMemberHasAppropriateForm = doc.data === null || Array.isArray(doc.data) || isSimpleObject(doc.data);
|
|
503
|
-
if (!dataMemberHasAppropriateForm) {
|
|
504
|
-
reporter.error(['data'], `The 'data' member MUST be a single resource object or an array of resource objects or null. Received data of type "${inspectType(doc.data)}"`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// 2. `included` MUST be an array of resource objects
|
|
509
|
-
if ('included' in doc) {
|
|
510
|
-
if (!Array.isArray(doc.included)) {
|
|
511
|
-
reporter.error(['included'], `The 'included' member MUST be an array of resource objects. Received data of type "${inspectType(doc.included)}"`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// 3. `meta` MUST be a simple object
|
|
516
|
-
if ('meta' in doc) {
|
|
517
|
-
if (!isSimpleObject(doc.meta)) {
|
|
518
|
-
reporter.error(['meta'], `The 'meta' member MUST be a simple object. Received data of type "${inspectType(doc.meta)}"`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// 4. `jsonapi` MUST be a simple object
|
|
523
|
-
if ('jsonapi' in doc) {
|
|
524
|
-
if (!isSimpleObject(doc.jsonapi)) {
|
|
525
|
-
reporter.error(['jsonapi'], `The 'jsonapi' member MUST be a simple object. Received data of type "${inspectType(doc.jsonapi)}"`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// 5. `links` MUST be a simple object
|
|
530
|
-
if ('links' in doc) {
|
|
531
|
-
if (!isSimpleObject(doc.links)) {
|
|
532
|
-
reporter.error(['links'], `The 'links' member MUST be a simple object. Received data of type "${inspectType(doc.links)}"`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// 6. `errors` MUST be an array of error objects
|
|
537
|
-
if ('errors' in doc) {
|
|
538
|
-
if (!Array.isArray(doc.errors)) {
|
|
539
|
-
reporter.error(['errors'], `The 'errors' member MUST be an array of error objects. Received data of type "${inspectType(doc.errors)}"`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const VALID_COLLECTION_LINKS = ['self', 'related', 'first', 'last', 'prev', 'next'];
|
|
545
|
-
const VALID_RESOURCE_RELATIONSHIP_LINKS = ['self', 'related'];
|
|
546
|
-
const VALID_RESOURCE_LINKS = ['self'];
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Validates the links object in a top-level JSON API document or resource object
|
|
550
|
-
*
|
|
551
|
-
* Version: 1.1
|
|
552
|
-
*
|
|
553
|
-
* Section: 7.1 Top Level
|
|
554
|
-
* Link: https://jsonapi.org/format/#document-top-level
|
|
555
|
-
*
|
|
556
|
-
* Section: 7.2.3 Resource Objects
|
|
557
|
-
* Link: https://jsonapi.org/format/#document-resource-object-links
|
|
558
|
-
*
|
|
559
|
-
* Section: 7.2.2.2 Resource Relationships
|
|
560
|
-
* Link: https://jsonapi.org/format/#document-resource-object-relationships
|
|
561
|
-
*
|
|
562
|
-
* Section: 7.6 Document Links
|
|
563
|
-
* Link: https://jsonapi.org/format/#document-links
|
|
564
|
-
*
|
|
565
|
-
* @internal
|
|
566
|
-
*/
|
|
567
|
-
function validateLinks(reporter, doc, type, path = ['links']) {
|
|
568
|
-
if (!('links' in doc)) {
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
if (!isSimpleObject(doc.links)) {
|
|
572
|
-
// this is a violation but we report it when validating section 7.1
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// prettier-ignore
|
|
577
|
-
const VALID_TOP_LEVEL_LINKS = type === 'collection-document' || type === 'collection-relationship' ? VALID_COLLECTION_LINKS : type === 'resource-document' || type === 'resource-relationship' ? VALID_RESOURCE_RELATIONSHIP_LINKS : type === 'resource' ? VALID_RESOURCE_LINKS : [];
|
|
578
|
-
const links = doc.links;
|
|
579
|
-
const keys = Object.keys(links);
|
|
580
|
-
for (const key of keys) {
|
|
581
|
-
if (!VALID_TOP_LEVEL_LINKS.includes(key)) {
|
|
582
|
-
reporter.warn([...path, key], `Unrecognized top-level link. The data it provides may be ignored as it is not a valid {json:api} link for a ${type}`);
|
|
583
|
-
}
|
|
584
|
-
// links may be either a string or an object with an href property or null
|
|
585
|
-
if (links[key] === null) ; else if (typeof links[key] === 'string') {
|
|
586
|
-
if (links[key].length === 0) {
|
|
587
|
-
reporter.warn([...path, key], `Expected a non-empty string, but received an empty string`);
|
|
588
|
-
}
|
|
589
|
-
// valid, though we should potentially validate the URL here
|
|
590
|
-
} else if (isSimpleObject(links[key])) {
|
|
591
|
-
if ('href' in links[key]) {
|
|
592
|
-
const linksKeys = Object.keys(links[key]);
|
|
593
|
-
if (linksKeys.length > 1) {
|
|
594
|
-
reporter.warn([...path, key], `Expected the links object to only have an href property, but received unknown keys ${linksKeys.filter(k => k !== 'href').join(', ')}`);
|
|
595
|
-
}
|
|
596
|
-
if (typeof links[key].href !== 'string') {
|
|
597
|
-
reporter.error([...path, key, 'href'], `Expected a string value, but received ${inspectType(links[key].href)}`);
|
|
598
|
-
} else {
|
|
599
|
-
if (links[key].href.length === 0) {
|
|
600
|
-
reporter.warn([...path, key, 'href'], `Expected a non-empty string, but received an empty string`);
|
|
601
|
-
}
|
|
602
|
-
// valid, though we should potentially validate the URL here
|
|
603
|
-
}
|
|
604
|
-
} else {
|
|
605
|
-
const linksKeys = Object.keys(links[key]);
|
|
606
|
-
if (linksKeys.length > 0) {
|
|
607
|
-
reporter.error([...path, key], `Expected the links object to have an href property, but received only the unknown keys ${linksKeys.join(', ')}`);
|
|
608
|
-
} else {
|
|
609
|
-
reporter.error([...path, key], `Expected the links object to have an href property`);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
} else {
|
|
613
|
-
// invalid
|
|
614
|
-
reporter.error([...path, key], `Expected a string, null, or an object with an href property for the link "${key}", but received ${inspectType(links[key])}`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const SINGULAR_OPS = ['createRecord', 'updateRecord', 'deleteRecord', 'findRecord', 'queryRecord'];
|
|
620
|
-
|
|
621
|
-
/**
|
|
622
|
-
* Validates the resource objects in either the `data` or `included` members of
|
|
623
|
-
* JSON:API document.
|
|
624
|
-
*
|
|
625
|
-
* Version: 1.1
|
|
626
|
-
* Section: 7.2
|
|
627
|
-
* Link: https://jsonapi.org/format/#document-resource-objects
|
|
628
|
-
*
|
|
629
|
-
* @internal
|
|
630
|
-
*/
|
|
631
|
-
function validateDocumentResources(reporter, doc) {
|
|
632
|
-
if ('data' in doc) {
|
|
633
|
-
// scan for common mistakes of single vs multiple resource objects
|
|
634
|
-
const op = reporter.contextDocument.request?.op;
|
|
635
|
-
if (op && SINGULAR_OPS.includes(op)) {
|
|
636
|
-
if (Array.isArray(doc.data)) {
|
|
637
|
-
reporter.error(['data'], `"${op}" requests expect a single resource object in the returned data, but received an array`);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// guard for a common mistake around deleteRecord
|
|
642
|
-
if (op === 'deleteRecord') {
|
|
643
|
-
if (doc.data !== null) {
|
|
644
|
-
reporter.warn(['data'], `"deleteRecord" requests expect the data member to be null, but received ${inspectType(doc.data)}. This can sometimes cause unexpected resurrection of the deleted record.`);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (Array.isArray(doc.data)) {
|
|
648
|
-
doc.data.forEach((resource, index) => {
|
|
649
|
-
if (!isSimpleObject(resource)) {
|
|
650
|
-
reporter.error(['data', index], `Expected a resource object, but received ${inspectType(resource)}`);
|
|
651
|
-
} else {
|
|
652
|
-
validateResourceObject(reporter, resource, ['data', index]);
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
} else if (doc.data !== null) {
|
|
656
|
-
if (!isSimpleObject(doc.data)) {
|
|
657
|
-
reporter.error(['data'], `Expected a resource object, but received ${inspectType(doc.data)}`);
|
|
658
|
-
} else {
|
|
659
|
-
validateResourceObject(reporter, doc.data, ['data']);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
if ('included' in doc && Array.isArray(doc.included)) {
|
|
664
|
-
doc.included.forEach((resource, index) => {
|
|
665
|
-
if (!isSimpleObject(resource)) {
|
|
666
|
-
reporter.error(['included', index], `Expected a resource object, but received ${inspectType(resource)}`);
|
|
667
|
-
} else {
|
|
668
|
-
validateResourceObject(reporter, resource, ['included', index]);
|
|
669
|
-
}
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
function validateResourceObject(reporter, resource, path) {
|
|
674
|
-
validateTopLevelResourceShape(reporter, resource, path);
|
|
675
|
-
}
|
|
676
|
-
const VALID_TOP_LEVEL_RESOURCE_KEYS = ['lid', 'id', 'type', 'attributes', 'relationships', 'meta', 'links'];
|
|
677
|
-
function validateTopLevelResourceShape(reporter, resource, path) {
|
|
678
|
-
// a resource MUST have a string type
|
|
679
|
-
if (!('type' in resource)) {
|
|
680
|
-
reporter.error([...path, 'type'], `Expected a ResourceObject to have a type property`);
|
|
681
|
-
} else if (typeof resource.type !== 'string') {
|
|
682
|
-
reporter.error([...path, 'type'], `Expected a string value for the type property, but received ${inspectType(resource.type)}`, 'value');
|
|
683
|
-
} else if (resource.type.length === 0) {
|
|
684
|
-
reporter.error([...path, 'type'], `Expected a non-empty string value for the type property, but received an empty string`, 'value');
|
|
685
|
-
} else if (!reporter.schema.hasResource({
|
|
686
|
-
type: resource.type
|
|
687
|
-
})) {
|
|
688
|
-
const method = reporter.strict.unknownType ? 'error' : 'warn';
|
|
689
|
-
const potentialTypes = reporter.searchTypes(resource.type);
|
|
690
|
-
reporter[method]([...path, 'type'], `Expected a schema to be available for the ResourceType "${resource.type}" but none was found.${logPotentialMatches(potentialTypes, 'ResourceType')}`, 'value');
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
// a resource MUST have a string ID
|
|
694
|
-
if (!('id' in resource)) {
|
|
695
|
-
reporter.error([...path, 'id'], `Expected a ResourceObject to have an id property`);
|
|
696
|
-
} else if (typeof resource.id !== 'string') {
|
|
697
|
-
reporter.error([...path, 'id'], `Expected a string value for the id property, but received ${inspectType(resource.id)}`, 'value');
|
|
698
|
-
} else if (resource.id.length === 0) {
|
|
699
|
-
reporter.error([...path, 'id'], `Expected a non-empty string value for the id property, but received an empty string`, 'value');
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// a resource MAY have a lid property
|
|
703
|
-
if ('lid' in resource && typeof resource.lid !== 'string') {
|
|
704
|
-
reporter.error([...path, 'lid'], `Expected a string value for the lid property, but received ${inspectType(resource.lid)}`, 'value');
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// a resource MAY have a meta property
|
|
708
|
-
if ('meta' in resource && !isSimpleObject(resource.meta)) {
|
|
709
|
-
reporter.error([...path, 'meta'], `Expected a simple object for the meta property, but received ${inspectType(resource.meta)}`, 'value');
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// a resource MAY have a links property
|
|
713
|
-
if ('links' in resource && !isSimpleObject(resource.links)) {
|
|
714
|
-
reporter.error([...path, 'links'], `Expected a simple object for the links property, but received ${inspectType(resource.links)}`, 'value');
|
|
715
|
-
} else if ('links' in resource) {
|
|
716
|
-
validateLinks(reporter, resource, 'resource', [...path, 'links']);
|
|
717
|
-
}
|
|
718
|
-
const hasAttributes = 'attributes' in resource && isSimpleObject(resource.attributes);
|
|
719
|
-
const hasRelationships = 'relationships' in resource && isSimpleObject(resource.relationships);
|
|
720
|
-
|
|
721
|
-
// We expect at least one of attributes or relationships to be present
|
|
722
|
-
if (!hasAttributes && !hasRelationships) {
|
|
723
|
-
reporter.warn(path, `Expected a ResourceObject to have either attributes or relationships`);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// we expect at least one of attributes or relationships to be non-empty
|
|
727
|
-
const attributesLength = hasAttributes ? Object.keys(resource.attributes).length : 0;
|
|
728
|
-
const relationshipsLength = hasRelationships ? Object.keys(resource.relationships).length : 0;
|
|
729
|
-
if ((hasAttributes || hasRelationships) && attributesLength === 0 && relationshipsLength === 0) {
|
|
730
|
-
reporter.warn([...path, hasAttributes ? 'attributes' : hasRelationships ? 'relationships' : 'attributes'], `Expected a ResourceObject to have either non-empty attributes or non-empty relationships`);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// check for unknown keys on the resource object
|
|
734
|
-
const keys = Object.keys(resource);
|
|
735
|
-
for (const key of keys) {
|
|
736
|
-
if (!VALID_TOP_LEVEL_RESOURCE_KEYS.includes(key)) {
|
|
737
|
-
// check for extension keys
|
|
738
|
-
if (key.includes(':')) {
|
|
739
|
-
const extensionName = key.split(':')[0];
|
|
740
|
-
if (reporter.hasExtension(extensionName)) {
|
|
741
|
-
const extension = reporter.getExtension(extensionName);
|
|
742
|
-
extension(reporter, [...path, key]);
|
|
743
|
-
} else {
|
|
744
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} ResourceObject member`);
|
|
745
|
-
}
|
|
746
|
-
} else {
|
|
747
|
-
// check if this is an attribute or relationship
|
|
748
|
-
let didYouMean = ' Likely this field should have been inside of either "attributes" or "relationships"';
|
|
749
|
-
const type = 'type' in resource ? resource.type : undefined;
|
|
750
|
-
if (type && reporter.schema.hasResource({
|
|
751
|
-
type
|
|
752
|
-
})) {
|
|
753
|
-
const fields = reporter.schema.fields({
|
|
754
|
-
type
|
|
755
|
-
});
|
|
756
|
-
const field = getRemoteField(fields, key);
|
|
757
|
-
if (field) {
|
|
758
|
-
const isRelationship = RELATIONSHIP_FIELD_KINDS.includes(field.kind);
|
|
759
|
-
didYouMean = ` Based on the ResourceSchema for "${type}" this field is likely a ${field.kind} and belongs inside of ${isRelationship ? 'relationships' : 'attributes'}, e.g. "${isRelationship ? 'relationships' : 'attributes'}": { "${key}": { ... } }`;
|
|
760
|
-
} else {
|
|
761
|
-
const fieldMatches = reporter.searchFields(type, key);
|
|
762
|
-
if (fieldMatches.length === 1) {
|
|
763
|
-
const matchedField = fields.get(fieldMatches[0].item);
|
|
764
|
-
const isRelationship = RELATIONSHIP_FIELD_KINDS.includes(matchedField.kind);
|
|
765
|
-
didYouMean = ` Based on the ResourceSchema for "${type}" this field is likely a ${matchedField.kind} and belongs inside of ${isRelationship ? 'relationships' : 'attributes'}, e.g. "${isRelationship ? 'relationships' : 'attributes'}": { "${matchedField.name}": { ... } }`;
|
|
766
|
-
} else if (fieldMatches.length > 1) {
|
|
767
|
-
const matchedField = fields.get(fieldMatches[0].item);
|
|
768
|
-
const isRelationship = RELATIONSHIP_FIELD_KINDS.includes(matchedField.kind);
|
|
769
|
-
didYouMean = ` Based on the ResourceSchema for "${type}" this field is likely one of "${fieldMatches.map(v => v.item).join('", "')}" and belongs inside of either "attributes" or "relationships", e.g. "${isRelationship ? 'relationships' : 'attributes'}": { "${matchedField.name}": { ... } }`;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
reporter.error([...path, key], `Unrecognized ResourceObject member. The data it provides is ignored as it is not a valid {json:api} ResourceObject member.${didYouMean}`);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// if we have a schema, validate the individual attributes and relationships
|
|
779
|
-
const type = 'type' in resource ? resource.type : undefined;
|
|
780
|
-
if (type && reporter.schema.hasResource({
|
|
781
|
-
type
|
|
782
|
-
})) {
|
|
783
|
-
if ('attributes' in resource) {
|
|
784
|
-
validateResourceAttributes(reporter, type, resource.attributes, [...path, 'attributes']);
|
|
785
|
-
}
|
|
786
|
-
if ('relationships' in resource) {
|
|
787
|
-
validateResourceRelationships(reporter, type, resource.relationships, [...path, 'relationships']);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
function validateResourceAttributes(reporter, type, resource, path) {
|
|
792
|
-
const fields = reporter.schema.fields({
|
|
793
|
-
type
|
|
794
|
-
});
|
|
795
|
-
const cacheFields = reporter.schema.cacheFields?.({
|
|
796
|
-
type
|
|
797
|
-
}) ?? fields;
|
|
798
|
-
for (const [key] of Object.entries(resource)) {
|
|
799
|
-
const field = getRemoteField(cacheFields, key);
|
|
800
|
-
const actualField = cacheFields.get(key);
|
|
801
|
-
if (!field && actualField) {
|
|
802
|
-
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.`);
|
|
803
|
-
} else if (!field) {
|
|
804
|
-
if (key.includes(':')) {
|
|
805
|
-
const extensionName = key.split(':')[0];
|
|
806
|
-
if (reporter.hasExtension(extensionName)) {
|
|
807
|
-
const extension = reporter.getExtension(extensionName);
|
|
808
|
-
extension(reporter, [...path, key]);
|
|
809
|
-
} else {
|
|
810
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} ResourceObject member`);
|
|
811
|
-
}
|
|
812
|
-
} else {
|
|
813
|
-
const method = reporter.strict.unknownAttribute ? 'error' : 'warn';
|
|
814
|
-
|
|
815
|
-
// TODO @runspired when we check for fuzzy matches we can adjust the message to say
|
|
816
|
-
// whether the expected field is an attribute or a relationship
|
|
817
|
-
const potentialFields = reporter.searchFields(type, key);
|
|
818
|
-
reporter[method]([...path, key], `Unrecognized attribute. The data it provides is ignored as it is not part of the ResourceSchema for "${type}".${logPotentialMatches(potentialFields, 'field')}`);
|
|
819
|
-
}
|
|
820
|
-
} else if (field && RELATIONSHIP_FIELD_KINDS.includes(field.kind)) {
|
|
821
|
-
reporter.error([...path, key], `Expected the "${key}" field to be in "relationships" as it has kind "${field.kind}", but received data for it in "attributes".`);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// TODO @runspired we should also deep-validate the field value
|
|
826
|
-
// TODO @runspired we should validate that field values are valid JSON and not instances
|
|
827
|
-
}
|
|
828
|
-
function validateResourceRelationships(reporter, type, resource, path) {
|
|
829
|
-
const schema = reporter.schema.fields({
|
|
830
|
-
type
|
|
831
|
-
});
|
|
832
|
-
for (const [key] of Object.entries(resource)) {
|
|
833
|
-
const field = getRemoteField(schema, key);
|
|
834
|
-
const actualField = schema.get(key);
|
|
835
|
-
if (!field && actualField) {
|
|
836
|
-
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.`);
|
|
837
|
-
} else if (!field) {
|
|
838
|
-
if (key.includes(':')) {
|
|
839
|
-
const extensionName = key.split(':')[0];
|
|
840
|
-
if (reporter.hasExtension(extensionName)) {
|
|
841
|
-
const extension = reporter.getExtension(extensionName);
|
|
842
|
-
extension(reporter, [...path, key]);
|
|
843
|
-
} else {
|
|
844
|
-
reporter.warn([...path, key], `Unrecognized extension ${extensionName}. The data provided by "${key}" will be ignored as it is not a valid {json:api} ResourceObject member`);
|
|
845
|
-
}
|
|
846
|
-
} else {
|
|
847
|
-
const method = reporter.strict.unknownRelationship ? 'error' : 'warn';
|
|
848
|
-
|
|
849
|
-
// TODO @runspired when we check for fuzzy matches we can adjust the message to say
|
|
850
|
-
// whether the expected field is an attribute or a relationship
|
|
851
|
-
const potentialFields = reporter.searchFields(type, key);
|
|
852
|
-
reporter[method]([...path, key], `Unrecognized relationship. The data it provides is ignored as it is not part of the ResourceSchema for "${type}".${logPotentialMatches(potentialFields, 'field')}`);
|
|
853
|
-
}
|
|
854
|
-
} else if (field && !RELATIONSHIP_FIELD_KINDS.includes(field.kind)) {
|
|
855
|
-
reporter.error([...path, key], `Expected the "${key}" field to be in "attributes" as it has kind "${field.kind}", but received data for it in "relationships".`);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// TODO @runspired we should also deep-validate the relationship payload
|
|
860
|
-
// TODO @runspired we should validate linksMode requirements for both Polaris and Legacy modes
|
|
861
|
-
// TODO @runspired we should warn if the discovered resource-type in a relationship is the abstract
|
|
862
|
-
// type instead of the concrete type.
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
function validateDocument(capabilities, doc) {
|
|
866
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
867
|
-
if (!test) {
|
|
868
|
-
throw new Error(`Expected a JSON:API Document as the content provided to the cache, received ${typeof doc.content}`);
|
|
869
|
-
}
|
|
870
|
-
})(doc instanceof Error || typeof doc.content === 'object' && doc.content !== null) : {};
|
|
871
|
-
|
|
872
|
-
// if the feature is not active and the payloads are not being logged
|
|
873
|
-
// we don't need to validate the payloads
|
|
874
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.features.JSON_API_CACHE_VALIDATION_ERRORS)) {
|
|
875
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
876
|
-
if (!(getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE)) {
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
882
|
-
if (!(getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE)) {
|
|
883
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.features.JSON_API_CACHE_VALIDATION_ERRORS)) {
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
if (isErrorDocument(doc)) {
|
|
889
|
-
return; // return validateErrorDocument(reporter, doc);
|
|
890
|
-
} else if (isMetaDocument(doc)) {
|
|
891
|
-
return; // return validateMetaDocument(reporter, doc);
|
|
892
|
-
} else if (isPushedDocument(doc)) {
|
|
893
|
-
return; // return validatePushedDocument(reporter, doc);
|
|
894
|
-
}
|
|
895
|
-
const reporter = new Reporter(capabilities, doc);
|
|
896
|
-
return validateResourceDocument(reporter, doc);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// function validateErrorDocument(reporter: Reporter, doc: StructuredErrorDocument) {}
|
|
900
|
-
|
|
901
|
-
// function validateMetaDocument(reporter: Reporter, doc: StructuredDataDocument<ResourceMetaDocument>) {}
|
|
902
|
-
|
|
903
|
-
// function validatePushedDocument(reporter: Reporter, doc: StructuredDataDocument<ResourceDocument>) {}
|
|
904
|
-
|
|
905
|
-
function validateResourceDocument(reporter, doc) {
|
|
906
|
-
validateTopLevelDocumentMembers(reporter, doc.content);
|
|
907
|
-
validateLinks(reporter, doc.content, 'data' in doc.content && Array.isArray(doc.content?.data) ? 'collection-document' : 'resource-document');
|
|
908
|
-
validateDocumentResources(reporter, doc.content);
|
|
909
|
-
|
|
910
|
-
// TODO @runspired - validateMeta on document
|
|
911
|
-
// TODO @runspired - validateMeta on resource
|
|
912
|
-
// TODO @runspired - validateMeta on resource relationships
|
|
913
|
-
// TODO @runspired - validate no-meta on resource identifiers
|
|
914
|
-
//
|
|
915
|
-
// ---------------------------------
|
|
916
|
-
// super-strict-mode
|
|
917
|
-
//
|
|
918
|
-
// TODO @runspired - validate that all referenced resource identifiers are present in the document (full linkage)
|
|
919
|
-
// TODO @runspired - validate that all included resources have a path back to `data` (full linkage)
|
|
920
|
-
//
|
|
921
|
-
// ---------------------------------
|
|
922
|
-
// nice-to-haves
|
|
923
|
-
//
|
|
924
|
-
// TODO @runspired - validate links objects more thoroughly for spec props we don't use
|
|
925
|
-
// TODO @runspired - validate request includes are in fact included
|
|
926
|
-
// TODO @runspired - validate request fields are in fact present
|
|
927
|
-
// TODO @runspired - MAYBE validate request sort is in fact sorted? (useful for catching Mocking bugs)
|
|
928
|
-
// TODO @runspired - MAYBE validate request pagination is in fact paginated? (useful for catching Mocking bugs)
|
|
929
|
-
|
|
930
|
-
reporter.report();
|
|
931
|
-
}
|
|
932
12
|
|
|
933
13
|
function isImplicit(relationship) {
|
|
934
14
|
return relationship.definition.isImplicit;
|
|
@@ -1051,9 +131,6 @@ class JSONAPICache {
|
|
|
1051
131
|
*/
|
|
1052
132
|
|
|
1053
133
|
put(doc) {
|
|
1054
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
1055
|
-
validateDocument(this._capabilities, doc);
|
|
1056
|
-
}
|
|
1057
134
|
if (isErrorDocument(doc)) {
|
|
1058
135
|
return this._putDocument(doc, undefined, undefined);
|
|
1059
136
|
} else if (isMetaDocument(doc)) {
|
|
@@ -1065,54 +142,6 @@ class JSONAPICache {
|
|
|
1065
142
|
const {
|
|
1066
143
|
cacheKeyManager
|
|
1067
144
|
} = this._capabilities;
|
|
1068
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
1069
|
-
validateDocumentFields(this._capabilities.schema, jsonApiDoc);
|
|
1070
|
-
}
|
|
1071
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1072
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1073
|
-
const Counts = new Map();
|
|
1074
|
-
let totalCount = 0;
|
|
1075
|
-
if (included) {
|
|
1076
|
-
for (i = 0, length = included.length; i < length; i++) {
|
|
1077
|
-
const type = included[i].type;
|
|
1078
|
-
Counts.set(type, (Counts.get(type) || 0) + 1);
|
|
1079
|
-
totalCount++;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
if (Array.isArray(jsonApiDoc.data)) {
|
|
1083
|
-
for (i = 0, length = jsonApiDoc.data.length; i < length; i++) {
|
|
1084
|
-
const type = jsonApiDoc.data[i].type;
|
|
1085
|
-
Counts.set(type, (Counts.get(type) || 0) + 1);
|
|
1086
|
-
totalCount++;
|
|
1087
|
-
}
|
|
1088
|
-
} else if (jsonApiDoc.data) {
|
|
1089
|
-
const type = jsonApiDoc.data.type;
|
|
1090
|
-
Counts.set(type, (Counts.get(type) || 0) + 1);
|
|
1091
|
-
totalCount++;
|
|
1092
|
-
}
|
|
1093
|
-
logGroup('cache', 'put', '<@document>', doc.content?.lid || doc.request?.url || 'unknown-request', `(${totalCount}) records`, '');
|
|
1094
|
-
let str = `\tContent Counts:`;
|
|
1095
|
-
Counts.forEach((count, type) => {
|
|
1096
|
-
str += `\n\t\t${type}: ${count} record${count > 1 ? 's' : ''}`;
|
|
1097
|
-
});
|
|
1098
|
-
if (Counts.size === 0) {
|
|
1099
|
-
str += `\t(empty)`;
|
|
1100
|
-
}
|
|
1101
|
-
// eslint-disable-next-line no-console
|
|
1102
|
-
console.log(str);
|
|
1103
|
-
// eslint-disable-next-line no-console
|
|
1104
|
-
console.log({
|
|
1105
|
-
lid: doc.content?.lid,
|
|
1106
|
-
content: structuredClone(doc.content),
|
|
1107
|
-
// we may need a specialized copy here
|
|
1108
|
-
request: doc.request,
|
|
1109
|
-
// structuredClone(doc.request),
|
|
1110
|
-
response: doc.response // structuredClone(doc.response),
|
|
1111
|
-
});
|
|
1112
|
-
// eslint-disable-next-line no-console
|
|
1113
|
-
console.groupEnd();
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
145
|
if (included) {
|
|
1117
146
|
for (i = 0, length = included.length; i < length; i++) {
|
|
1118
147
|
included[i] = putOne(this, cacheKeyManager, included[i]);
|
|
@@ -1149,16 +178,6 @@ class JSONAPICache {
|
|
|
1149
178
|
resourceDocument.data = data;
|
|
1150
179
|
}
|
|
1151
180
|
if (included !== undefined) {
|
|
1152
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1153
|
-
if (!test) {
|
|
1154
|
-
throw new Error(`There should not be included data on an Error document`);
|
|
1155
|
-
}
|
|
1156
|
-
})(!isErrorDocument(doc)) : {};
|
|
1157
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1158
|
-
if (!test) {
|
|
1159
|
-
throw new Error(`There should not be included data on a Meta document`);
|
|
1160
|
-
}
|
|
1161
|
-
})(!isMetaDocument(doc)) : {};
|
|
1162
181
|
resourceDocument.included = included;
|
|
1163
182
|
}
|
|
1164
183
|
const request = doc.request;
|
|
@@ -1175,16 +194,6 @@ class JSONAPICache {
|
|
|
1175
194
|
if (doc.request?.op === 'findHasMany') {
|
|
1176
195
|
const parentIdentifier = doc.request.options?.identifier;
|
|
1177
196
|
const parentField = doc.request.options?.field;
|
|
1178
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1179
|
-
if (!test) {
|
|
1180
|
-
throw new Error(`Expected a hasMany field`);
|
|
1181
|
-
}
|
|
1182
|
-
})(parentField?.kind === 'hasMany') : {};
|
|
1183
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1184
|
-
if (!test) {
|
|
1185
|
-
throw new Error(`Expected a parent identifier for a findHasMany request`);
|
|
1186
|
-
}
|
|
1187
|
-
})(parentIdentifier && isResourceKey(parentIdentifier)) : {};
|
|
1188
197
|
if (parentField && parentIdentifier) {
|
|
1189
198
|
this.__graph.push({
|
|
1190
199
|
op: 'updateRelationship',
|
|
@@ -1207,23 +216,12 @@ class JSONAPICache {
|
|
|
1207
216
|
*/
|
|
1208
217
|
patch(op) {
|
|
1209
218
|
if (Array.isArray(op)) {
|
|
1210
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1211
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1212
|
-
logGroup('cache', 'patch', '<BATCH>', String(op.length) + ' operations', '', '');
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
219
|
assertPrivateCapabilities(this._capabilities);
|
|
1216
220
|
this._capabilities._store._join(() => {
|
|
1217
221
|
for (const operation of op) {
|
|
1218
222
|
patchCache(this, operation);
|
|
1219
223
|
}
|
|
1220
224
|
});
|
|
1221
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1222
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1223
|
-
// eslint-disable-next-line no-console
|
|
1224
|
-
console.groupEnd();
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
225
|
} else {
|
|
1228
226
|
patchCache(this, op);
|
|
1229
227
|
}
|
|
@@ -1236,26 +234,7 @@ class JSONAPICache {
|
|
|
1236
234
|
* @public
|
|
1237
235
|
*/
|
|
1238
236
|
mutate(mutation) {
|
|
1239
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1240
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1241
|
-
logGroup('cache', 'mutate', mutation.record.type, mutation.record.lid, mutation.field, mutation.op);
|
|
1242
|
-
try {
|
|
1243
|
-
const _data = JSON.parse(JSON.stringify(mutation));
|
|
1244
|
-
// eslint-disable-next-line no-console
|
|
1245
|
-
console.log(_data);
|
|
1246
|
-
} catch {
|
|
1247
|
-
// eslint-disable-next-line no-console
|
|
1248
|
-
console.log(mutation);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
237
|
this.__graph.update(mutation, false);
|
|
1253
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1254
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1255
|
-
// eslint-disable-next-line no-console
|
|
1256
|
-
console.groupEnd();
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
238
|
}
|
|
1260
239
|
|
|
1261
240
|
/**
|
|
@@ -1551,18 +530,6 @@ class JSONAPICache {
|
|
|
1551
530
|
* @public
|
|
1552
531
|
*/
|
|
1553
532
|
clientDidCreate(identifier, options) {
|
|
1554
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1555
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1556
|
-
try {
|
|
1557
|
-
const _data = options ? JSON.parse(JSON.stringify(options)) : options;
|
|
1558
|
-
// eslint-disable-next-line no-console
|
|
1559
|
-
console.log(`WarpDrive | Mutation - clientDidCreate ${identifier.lid}`, _data);
|
|
1560
|
-
} catch {
|
|
1561
|
-
// eslint-disable-next-line no-console
|
|
1562
|
-
console.log(`WarpDrive | Mutation - clientDidCreate ${identifier.lid}`, options);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
533
|
const cached = this._createCache(identifier);
|
|
1567
534
|
cached.isNew = true;
|
|
1568
535
|
const createOptions = {};
|
|
@@ -1645,28 +612,9 @@ class JSONAPICache {
|
|
|
1645
612
|
const payload = result ? result.content : null;
|
|
1646
613
|
const operation = result?.request?.op ?? null;
|
|
1647
614
|
const data = payload && payload.data;
|
|
1648
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
1649
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
1650
|
-
try {
|
|
1651
|
-
const payloadCopy = payload ? JSON.parse(JSON.stringify(payload)) : payload;
|
|
1652
|
-
// eslint-disable-next-line no-console
|
|
1653
|
-
console.log(`WarpDrive | Payload - ${operation}`, payloadCopy);
|
|
1654
|
-
} catch {
|
|
1655
|
-
// eslint-disable-next-line no-console
|
|
1656
|
-
console.log(`WarpDrive | Payload - ${operation}`, payload);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
615
|
const responseIsCollection = Array.isArray(data);
|
|
1661
616
|
const hasMultipleIdentifiers = Array.isArray(committedIdentifier) && committedIdentifier.length > 1;
|
|
1662
617
|
if (Array.isArray(committedIdentifier)) {
|
|
1663
|
-
// if we get back an array of primary data, we treat each
|
|
1664
|
-
// entry as a separate commit for each identifier
|
|
1665
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1666
|
-
if (!test) {
|
|
1667
|
-
throw new Error(`Expected the array of primary data to match the array of committed identifiers`);
|
|
1668
|
-
}
|
|
1669
|
-
})(!hasMultipleIdentifiers || !responseIsCollection || data.length === committedIdentifier.length) : {};
|
|
1670
618
|
if (responseIsCollection) {
|
|
1671
619
|
for (let i = 0; i < committedIdentifier.length; i++) {
|
|
1672
620
|
const identifier = committedIdentifier[i];
|
|
@@ -1811,11 +759,6 @@ class JSONAPICache {
|
|
|
1811
759
|
if (isSimplePath) {
|
|
1812
760
|
const attribute = attr;
|
|
1813
761
|
const cached = this.__peek(identifier, true);
|
|
1814
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1815
|
-
if (!test) {
|
|
1816
|
-
throw new Error(`Cannot retrieve attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1817
|
-
}
|
|
1818
|
-
})(cached) : {};
|
|
1819
762
|
|
|
1820
763
|
// in Prod we try to recover when accessing something that
|
|
1821
764
|
// doesn't exist
|
|
@@ -1880,11 +823,6 @@ class JSONAPICache {
|
|
|
1880
823
|
if (isSimplePath) {
|
|
1881
824
|
const attribute = attr;
|
|
1882
825
|
const cached = this.__peek(identifier, true);
|
|
1883
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1884
|
-
if (!test) {
|
|
1885
|
-
throw new Error(`Cannot retrieve remote attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
1886
|
-
}
|
|
1887
|
-
})(cached) : {};
|
|
1888
826
|
|
|
1889
827
|
// in Prod we try to recover when accessing something that
|
|
1890
828
|
// doesn't exist
|
|
@@ -1936,12 +874,6 @@ class JSONAPICache {
|
|
|
1936
874
|
* @public
|
|
1937
875
|
*/
|
|
1938
876
|
setAttr(identifier, attr, value) {
|
|
1939
|
-
// this assert works to ensure we have a non-empty string and/or a non-empty array
|
|
1940
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
1941
|
-
if (!test) {
|
|
1942
|
-
throw new Error('setAttr must receive at least one attribute path');
|
|
1943
|
-
}
|
|
1944
|
-
})(attr.length > 0) : {};
|
|
1945
877
|
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
|
|
1946
878
|
if (Array.isArray(attr) && attr.length === 1) {
|
|
1947
879
|
attr = attr[0];
|
|
@@ -2035,11 +967,6 @@ class JSONAPICache {
|
|
|
2035
967
|
*/
|
|
2036
968
|
changedAttrs(identifier) {
|
|
2037
969
|
const cached = this.__peek(identifier, false);
|
|
2038
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2039
|
-
if (!test) {
|
|
2040
|
-
throw new Error(`Cannot retrieve changed attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
2041
|
-
}
|
|
2042
|
-
})(cached) : {};
|
|
2043
970
|
|
|
2044
971
|
// in Prod we try to recover when accessing something that
|
|
2045
972
|
// doesn't exist
|
|
@@ -2059,11 +986,6 @@ class JSONAPICache {
|
|
|
2059
986
|
*/
|
|
2060
987
|
hasChangedAttrs(identifier) {
|
|
2061
988
|
const cached = this.__peek(identifier, true);
|
|
2062
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2063
|
-
if (!test) {
|
|
2064
|
-
throw new Error(`Cannot retrieve changed attributes for identifier ${String(identifier)} as it is not present in the cache`);
|
|
2065
|
-
}
|
|
2066
|
-
})(cached) : {};
|
|
2067
989
|
|
|
2068
990
|
// in Prod we try to recover when accessing something that
|
|
2069
991
|
// doesn't exist
|
|
@@ -2274,11 +1196,6 @@ class JSONAPICache {
|
|
|
2274
1196
|
* @internal
|
|
2275
1197
|
*/
|
|
2276
1198
|
_createCache(identifier) {
|
|
2277
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2278
|
-
if (!test) {
|
|
2279
|
-
throw new Error(`Expected no resource data to yet exist in the cache`);
|
|
2280
|
-
}
|
|
2281
|
-
})(!this.__cache.has(identifier)) : {};
|
|
2282
1199
|
const cache = makeCache();
|
|
2283
1200
|
this.__cache.set(identifier, cache);
|
|
2284
1201
|
return cache;
|
|
@@ -2306,62 +1223,22 @@ class JSONAPICache {
|
|
|
2306
1223
|
*/
|
|
2307
1224
|
__peek(identifier, allowDestroyed) {
|
|
2308
1225
|
const resource = this.__safePeek(identifier, allowDestroyed);
|
|
2309
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2310
|
-
if (!test) {
|
|
2311
|
-
throw new Error(`Expected Cache to have a resource entry for the identifier ${String(identifier)} but none was found`);
|
|
2312
|
-
}
|
|
2313
|
-
})(resource) : {};
|
|
2314
1226
|
return resource;
|
|
2315
1227
|
}
|
|
2316
1228
|
}
|
|
2317
1229
|
function addResourceToDocument(cache, op) {
|
|
2318
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2319
|
-
if (!test) {
|
|
2320
|
-
throw new Error(`Expected field to be either 'data' or 'included'`);
|
|
2321
|
-
}
|
|
2322
|
-
})(op.field === 'data' || op.field === 'included') : {};
|
|
2323
1230
|
const doc = cache.__documents.get(op.record.lid);
|
|
2324
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2325
|
-
if (!test) {
|
|
2326
|
-
throw new Error(`Expected to have a cached document on which to perform the add operation`);
|
|
2327
|
-
}
|
|
2328
|
-
})(doc) : {};
|
|
2329
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2330
|
-
if (!test) {
|
|
2331
|
-
throw new Error(`Expected to have content on the document`);
|
|
2332
|
-
}
|
|
2333
|
-
})(doc.content) : {};
|
|
2334
1231
|
const {
|
|
2335
1232
|
content
|
|
2336
1233
|
} = doc;
|
|
2337
1234
|
if (op.field === 'data') {
|
|
2338
1235
|
let shouldNotify = false;
|
|
2339
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2340
|
-
if (!test) {
|
|
2341
|
-
throw new Error(`Expected to have a data property on the document`);
|
|
2342
|
-
}
|
|
2343
|
-
})('data' in content) : {};
|
|
2344
1236
|
|
|
2345
1237
|
// if data is not an array, we set the data property directly
|
|
2346
1238
|
if (!Array.isArray(content.data)) {
|
|
2347
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2348
|
-
if (!test) {
|
|
2349
|
-
throw new Error(`Expected to have a single record as the operation value`);
|
|
2350
|
-
}
|
|
2351
|
-
})(op.value && !Array.isArray(op.value)) : {};
|
|
2352
1239
|
shouldNotify = content.data !== op.value;
|
|
2353
1240
|
if (shouldNotify) content.data = op.value;
|
|
2354
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2355
|
-
if (!test) {
|
|
2356
|
-
throw new Error(`The value '${op.value.lid}' cannot be added from the data of document '${op.record.lid}' as it is already the current value '${content.data ? content.data.lid : '<null>'}'`);
|
|
2357
|
-
}
|
|
2358
|
-
})(shouldNotify) : {};
|
|
2359
1241
|
} else {
|
|
2360
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2361
|
-
if (!test) {
|
|
2362
|
-
throw new Error(`Expected to have a non-null operation value`);
|
|
2363
|
-
}
|
|
2364
|
-
})(op.value) : {};
|
|
2365
1242
|
if (Array.isArray(op.value)) {
|
|
2366
1243
|
if (op.index !== undefined) {
|
|
2367
1244
|
// for collections, because we allow duplicates we are always changed.
|
|
@@ -2390,26 +1267,9 @@ function addResourceToDocument(cache, op) {
|
|
|
2390
1267
|
return;
|
|
2391
1268
|
}
|
|
2392
1269
|
content.included = content.included || [];
|
|
2393
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2394
|
-
if (!test) {
|
|
2395
|
-
throw new Error(`Expected to have a non-null operation value`);
|
|
2396
|
-
}
|
|
2397
|
-
})(op.value) : {};
|
|
2398
1270
|
if (Array.isArray(op.value)) {
|
|
2399
|
-
// included is not allowed to have duplicates, so we do a dirty check here
|
|
2400
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2401
|
-
if (!test) {
|
|
2402
|
-
throw new Error(`included should not contain duplicate members`);
|
|
2403
|
-
}
|
|
2404
|
-
})(new Set([...content.included, ...op.value]).size === content.included.length + op.value.length) : {};
|
|
2405
1271
|
content.included = content.included.concat(op.value);
|
|
2406
1272
|
} else {
|
|
2407
|
-
// included is not allowed to have duplicates, so we do a dirty check here
|
|
2408
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2409
|
-
if (!test) {
|
|
2410
|
-
throw new Error(`included should not contain duplicate members`);
|
|
2411
|
-
}
|
|
2412
|
-
})(content.included.includes(op.value) === false) : {};
|
|
2413
1273
|
content.included.push(op.value);
|
|
2414
1274
|
}
|
|
2415
1275
|
|
|
@@ -2417,54 +1277,19 @@ function addResourceToDocument(cache, op) {
|
|
|
2417
1277
|
// exposed. We should possibly consider doing so though for subscribers
|
|
2418
1278
|
}
|
|
2419
1279
|
function removeResourceFromDocument(cache, op) {
|
|
2420
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2421
|
-
if (!test) {
|
|
2422
|
-
throw new Error(`Expected field to be either 'data' or 'included'`);
|
|
2423
|
-
}
|
|
2424
|
-
})(op.field === 'data' || op.field === 'included') : {};
|
|
2425
1280
|
const doc = cache.__documents.get(op.record.lid);
|
|
2426
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2427
|
-
if (!test) {
|
|
2428
|
-
throw new Error(`Expected to have a cached document on which to perform the remove operation`);
|
|
2429
|
-
}
|
|
2430
|
-
})(doc) : {};
|
|
2431
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2432
|
-
if (!test) {
|
|
2433
|
-
throw new Error(`Expected to have content on the document`);
|
|
2434
|
-
}
|
|
2435
|
-
})(doc.content) : {};
|
|
2436
1281
|
const {
|
|
2437
1282
|
content
|
|
2438
1283
|
} = doc;
|
|
2439
1284
|
if (op.field === 'data') {
|
|
2440
1285
|
let shouldNotify = false;
|
|
2441
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2442
|
-
if (!test) {
|
|
2443
|
-
throw new Error(`Expected to have a data property on the document`);
|
|
2444
|
-
}
|
|
2445
|
-
})('data' in content) : {};
|
|
2446
1286
|
|
|
2447
1287
|
// if data is not an array, we set the data property directly
|
|
2448
1288
|
if (!Array.isArray(content.data)) {
|
|
2449
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2450
|
-
if (!test) {
|
|
2451
|
-
throw new Error(`Expected to have a single record as the operation value`);
|
|
2452
|
-
}
|
|
2453
|
-
})(op.value && !Array.isArray(op.value)) : {};
|
|
2454
1289
|
shouldNotify = content.data === op.value;
|
|
2455
1290
|
// we only remove the value if it was our existing value
|
|
2456
1291
|
if (shouldNotify) content.data = null;
|
|
2457
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2458
|
-
if (!test) {
|
|
2459
|
-
throw new Error(`The value '${op.value.lid}' cannot be removed from the data of document '${op.record.lid}' as it is not the current value '${content.data ? content.data.lid : '<null>'}'`);
|
|
2460
|
-
}
|
|
2461
|
-
})(shouldNotify) : {};
|
|
2462
1292
|
} else {
|
|
2463
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2464
|
-
if (!test) {
|
|
2465
|
-
throw new Error(`Expected to have a non-null operation value`);
|
|
2466
|
-
}
|
|
2467
|
-
})(op.value) : {};
|
|
2468
1293
|
const toRemove = Array.isArray(op.value) ? op.value : [op.value];
|
|
2469
1294
|
for (let i = 0; i < toRemove.length; i++) {
|
|
2470
1295
|
const value = toRemove[i];
|
|
@@ -2472,11 +1297,6 @@ function removeResourceFromDocument(cache, op) {
|
|
|
2472
1297
|
// in production we want to recover gracefully
|
|
2473
1298
|
// so we fallback to first-index-of
|
|
2474
1299
|
const index = op.index < content.data.length && content.data[op.index] === value ? op.index : content.data.indexOf(value);
|
|
2475
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2476
|
-
if (!test) {
|
|
2477
|
-
throw new Error(`Mismatched Index: Expected index '${op.index}' to contain the value '${value.lid}' but that value is at index '${index}'`);
|
|
2478
|
-
}
|
|
2479
|
-
})(op.index < content.data.length && content.data[op.index] === value) : {};
|
|
2480
1300
|
if (index !== -1) {
|
|
2481
1301
|
// we remove the first occurrence of the value
|
|
2482
1302
|
shouldNotify = true;
|
|
@@ -2497,24 +1317,9 @@ function removeResourceFromDocument(cache, op) {
|
|
|
2497
1317
|
if (shouldNotify) cache._capabilities.notifyChange(op.record, 'updated', null);
|
|
2498
1318
|
} else {
|
|
2499
1319
|
content.included = content.included || [];
|
|
2500
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2501
|
-
if (!test) {
|
|
2502
|
-
throw new Error(`Expected to have a non-null operation value`);
|
|
2503
|
-
}
|
|
2504
|
-
})(op.value) : {};
|
|
2505
1320
|
const toRemove = Array.isArray(op.value) ? op.value : [op.value];
|
|
2506
1321
|
for (const identifier of toRemove) {
|
|
2507
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2508
|
-
if (!test) {
|
|
2509
|
-
throw new Error(`attempted to remove a value from included that was not present in the included array`);
|
|
2510
|
-
}
|
|
2511
|
-
})(content.included.includes(identifier)) : {};
|
|
2512
1322
|
const index = content.included.indexOf(identifier);
|
|
2513
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2514
|
-
if (!test) {
|
|
2515
|
-
throw new Error(`The value '${identifier.lid}' cannot be removed from the included of document '${op.record.lid}' as it is not present`);
|
|
2516
|
-
}
|
|
2517
|
-
})(index !== -1) : {};
|
|
2518
1323
|
if (index !== -1) {
|
|
2519
1324
|
content.included.splice(index, 1);
|
|
2520
1325
|
}
|
|
@@ -2570,11 +1375,6 @@ function getDefaultValue(schema, identifier, store) {
|
|
|
2570
1375
|
// legacy support for defaultValues that are primitives
|
|
2571
1376
|
} else if (options && 'defaultValue' in options) {
|
|
2572
1377
|
const defaultValue = options.defaultValue;
|
|
2573
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2574
|
-
if (!test) {
|
|
2575
|
-
throw new Error(`Non primitive defaultValues are not supported because they are shared between all instances. If you would like to use a complex object as a default value please provide a function that returns the complex object.`);
|
|
2576
|
-
}
|
|
2577
|
-
})(typeof defaultValue !== 'object' || defaultValue === null) : {};
|
|
2578
1378
|
return defaultValue;
|
|
2579
1379
|
|
|
2580
1380
|
// new style transforms
|
|
@@ -2723,16 +1523,6 @@ function patchLocalAttributes(cached, changedRemoteKeys) {
|
|
|
2723
1523
|
return hasAppliedPatch;
|
|
2724
1524
|
}
|
|
2725
1525
|
function putOne(cache, identifiers, resource) {
|
|
2726
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2727
|
-
if (!test) {
|
|
2728
|
-
throw new Error(`You must include an 'id' for the resource data ${resource.type}`);
|
|
2729
|
-
}
|
|
2730
|
-
})(resource.id !== null && resource.id !== undefined && resource.id !== '') : {};
|
|
2731
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2732
|
-
if (!test) {
|
|
2733
|
-
throw new Error(`Missing Resource Type: received resource data with a type '${resource.type}' but no schema could be found with that name.`);
|
|
2734
|
-
}
|
|
2735
|
-
})(cache._capabilities.schema.hasResource(resource)) : {};
|
|
2736
1526
|
let identifier = identifiers.peekResourceKey(resource);
|
|
2737
1527
|
if (identifier) {
|
|
2738
1528
|
identifier = identifiers.updateRecordIdentifier(identifier, resource);
|
|
@@ -2866,20 +1656,6 @@ function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
|
2866
1656
|
const cached = peeked || cache._createCache(identifier);
|
|
2867
1657
|
const isLoading = /*#__NOINLINE__*/_isLoading(peeked, cache._capabilities, identifier) || !recordIsLoaded(peeked);
|
|
2868
1658
|
const isUpdate = /*#__NOINLINE__*/!_isEmpty(peeked) && !isLoading;
|
|
2869
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
2870
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
2871
|
-
logGroup('cache', 'upsert', identifier.type, identifier.lid, existed ? 'merged' : 'inserted', calculateChanges ? 'has-subscription' : '');
|
|
2872
|
-
try {
|
|
2873
|
-
const _data = JSON.parse(JSON.stringify(data));
|
|
2874
|
-
|
|
2875
|
-
// eslint-disable-next-line no-console
|
|
2876
|
-
console.log(_data);
|
|
2877
|
-
} catch {
|
|
2878
|
-
// eslint-disable-next-line no-console
|
|
2879
|
-
console.log(data);
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
1659
|
if (cached.isNew) {
|
|
2884
1660
|
cached.isNew = false;
|
|
2885
1661
|
cache._capabilities.notifyChange(identifier, 'identity', null);
|
|
@@ -2910,35 +1686,11 @@ function cacheUpsert(cache, identifier, data, calculateChanges) {
|
|
|
2910
1686
|
if (changedKeys?.size) {
|
|
2911
1687
|
notifyAttributes(cache._capabilities, identifier, changedKeys);
|
|
2912
1688
|
}
|
|
2913
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
2914
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
2915
|
-
// eslint-disable-next-line no-console
|
|
2916
|
-
console.groupEnd();
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
1689
|
return changedKeys?.size ? Array.from(changedKeys) : undefined;
|
|
2920
1690
|
}
|
|
2921
1691
|
function patchCache(Cache, op) {
|
|
2922
1692
|
const isRecord = isResourceKey(op.record);
|
|
2923
|
-
|
|
2924
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2925
|
-
if (!test) {
|
|
2926
|
-
throw new Error(`Expected Cache.patch op.record to be a record or document identifier`);
|
|
2927
|
-
}
|
|
2928
|
-
})(isRecord || isDocument) : {};
|
|
2929
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
2930
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
2931
|
-
logGroup('cache', 'patch', isRecord ? op.record.type : '<@document>', op.record.lid, op.op, 'field' in op ? op.field : op.op === 'mergeIdentifiers' ? op.value.lid : '');
|
|
2932
|
-
try {
|
|
2933
|
-
const _data = JSON.parse(JSON.stringify(op));
|
|
2934
|
-
// eslint-disable-next-line no-console
|
|
2935
|
-
console.log(_data);
|
|
2936
|
-
} catch {
|
|
2937
|
-
// eslint-disable-next-line no-console
|
|
2938
|
-
console.log(op);
|
|
2939
|
-
}
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
1693
|
+
!isRecord && isRequestKey(op.record);
|
|
2942
1694
|
switch (op.op) {
|
|
2943
1695
|
case 'mergeIdentifiers':
|
|
2944
1696
|
{
|
|
@@ -2955,11 +1707,6 @@ function patchCache(Cache, op) {
|
|
|
2955
1707
|
if (isRecord) {
|
|
2956
1708
|
if ('field' in op) {
|
|
2957
1709
|
const field = getCacheFields(Cache, op.record).get(op.field);
|
|
2958
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2959
|
-
if (!test) {
|
|
2960
|
-
throw new Error(`Expected ${op.field} to be a field on ${op.record.type}`);
|
|
2961
|
-
}
|
|
2962
|
-
})(field) : {};
|
|
2963
1710
|
if (isRelationship(field)) {
|
|
2964
1711
|
Cache.__graph.push(op);
|
|
2965
1712
|
} else {
|
|
@@ -2974,12 +1721,6 @@ function patchCache(Cache, op) {
|
|
|
2974
1721
|
} else {
|
|
2975
1722
|
Cache.upsert(op.record, op.value, Cache._capabilities.hasRecord(op.record));
|
|
2976
1723
|
}
|
|
2977
|
-
} else {
|
|
2978
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2979
|
-
{
|
|
2980
|
-
throw new Error(`Update operations on documents is not supported`);
|
|
2981
|
-
}
|
|
2982
|
-
})() : {};
|
|
2983
1724
|
}
|
|
2984
1725
|
break;
|
|
2985
1726
|
}
|
|
@@ -2992,11 +1733,6 @@ function patchCache(Cache, op) {
|
|
|
2992
1733
|
Cache.upsert(op.record, op.value, Cache._capabilities.hasRecord(op.record));
|
|
2993
1734
|
}
|
|
2994
1735
|
} else {
|
|
2995
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
2996
|
-
if (!test) {
|
|
2997
|
-
throw new Error(`Expected a field in the add operation`);
|
|
2998
|
-
}
|
|
2999
|
-
})('field' in op) : {};
|
|
3000
1736
|
addResourceToDocument(Cache, op);
|
|
3001
1737
|
}
|
|
3002
1738
|
break;
|
|
@@ -3022,39 +1758,11 @@ function patchCache(Cache, op) {
|
|
|
3022
1758
|
}
|
|
3023
1759
|
} else {
|
|
3024
1760
|
if ('field' in op) {
|
|
3025
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3026
|
-
if (!test) {
|
|
3027
|
-
throw new Error(`Expected a field in the remove operation`);
|
|
3028
|
-
}
|
|
3029
|
-
})('field' in op) : {};
|
|
3030
1761
|
removeResourceFromDocument(Cache, op);
|
|
3031
|
-
} else {
|
|
3032
|
-
// TODO @runspired teardown associated state ... notify subscribers etc.
|
|
3033
|
-
// This likely means that the instance cache needs to handle
|
|
3034
|
-
// holding onto reactive documents instead of the CacheHandler
|
|
3035
|
-
// and use a subscription to remove them.
|
|
3036
|
-
// Cache.__documents.delete(op.record.lid);
|
|
3037
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3038
|
-
{
|
|
3039
|
-
throw new Error(`Removing documents from the cache is not yet supported`);
|
|
3040
|
-
}
|
|
3041
|
-
})() : {};
|
|
3042
1762
|
}
|
|
3043
1763
|
}
|
|
3044
1764
|
break;
|
|
3045
1765
|
}
|
|
3046
|
-
default:
|
|
3047
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3048
|
-
{
|
|
3049
|
-
throw new Error(`Unhandled cache.patch operation ${op.op}`);
|
|
3050
|
-
}
|
|
3051
|
-
})() : {};
|
|
3052
|
-
}
|
|
3053
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.activeLogging.LOG_CACHE)) {
|
|
3054
|
-
if (getGlobalConfig().WarpDriveMirror.debug.LOG_CACHE || globalThis.getWarpDriveRuntimeConfig().debug.LOG_CACHE) {
|
|
3055
|
-
// eslint-disable-next-line no-console
|
|
3056
|
-
console.groupEnd();
|
|
3057
|
-
}
|
|
3058
1766
|
}
|
|
3059
1767
|
}
|
|
3060
1768
|
function getCacheFields(cache, identifier) {
|
|
@@ -3085,13 +1793,6 @@ function commitDidError(cache, identifier, errors) {
|
|
|
3085
1793
|
cache._capabilities.notifyChange(identifier, 'errors', null);
|
|
3086
1794
|
}
|
|
3087
1795
|
function didCommit(cache, committedIdentifier, data, op) {
|
|
3088
|
-
if (!data) {
|
|
3089
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3090
|
-
if (!test) {
|
|
3091
|
-
throw new Error(`Your ${committedIdentifier.type} record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response.`);
|
|
3092
|
-
}
|
|
3093
|
-
})(committedIdentifier.id) : {};
|
|
3094
|
-
}
|
|
3095
1796
|
const {
|
|
3096
1797
|
cacheKeyManager
|
|
3097
1798
|
} = cache._capabilities;
|
|
@@ -3109,16 +1810,6 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
3109
1810
|
cache._capabilities.notifyChange(identifier, 'removed', null);
|
|
3110
1811
|
// TODO @runspired should we early exit here?
|
|
3111
1812
|
}
|
|
3112
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3113
|
-
if (cached.isNew && !identifier.id && (typeof data?.id !== 'string' || data.id.length > 0)) {
|
|
3114
|
-
const error = new Error(`Expected an id ${String(identifier)} in response ${JSON.stringify(data)}`);
|
|
3115
|
-
//@ts-expect-error
|
|
3116
|
-
error.isAdapterError = true;
|
|
3117
|
-
//@ts-expect-error
|
|
3118
|
-
error.code = 'InvalidError';
|
|
3119
|
-
throw error;
|
|
3120
|
-
}
|
|
3121
|
-
}
|
|
3122
1813
|
const fields = getCacheFields(cache, identifier);
|
|
3123
1814
|
cached.isNew = false;
|
|
3124
1815
|
let newCanonicalAttributes;
|
|
@@ -3129,36 +1820,7 @@ function didCommit(cache, committedIdentifier, data, op) {
|
|
|
3129
1820
|
if (identifier === committedIdentifier && identifier.id !== existingId) {
|
|
3130
1821
|
cache._capabilities.notifyChange(identifier, 'identity', null);
|
|
3131
1822
|
}
|
|
3132
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3133
|
-
if (!test) {
|
|
3134
|
-
throw new Error(`Expected the ID received for the primary '${identifier.type}' resource being saved to match the current id '${cached.id}' but received '${identifier.id}'.`);
|
|
3135
|
-
}
|
|
3136
|
-
})(identifier.id === cached.id) : {};
|
|
3137
1823
|
if (data.relationships) {
|
|
3138
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3139
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3140
|
-
// assert against bad API behavior where a belongsTo relationship
|
|
3141
|
-
// is saved but the return payload indicates a different final state.
|
|
3142
|
-
fields.forEach((field, name) => {
|
|
3143
|
-
if (field.kind === 'belongsTo') {
|
|
3144
|
-
const relationshipData = data.relationships[name]?.data;
|
|
3145
|
-
if (relationshipData !== undefined) {
|
|
3146
|
-
const inFlightData = cached.inflightRelationships?.[name];
|
|
3147
|
-
if (!inFlightData || !('data' in inFlightData)) {
|
|
3148
|
-
return;
|
|
3149
|
-
}
|
|
3150
|
-
const actualData = relationshipData ? cache._capabilities.cacheKeyManager.getOrCreateRecordIdentifier(relationshipData) : null;
|
|
3151
|
-
macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG) ? (test => {
|
|
3152
|
-
if (!test) {
|
|
3153
|
-
throw new Error(`Expected the resource relationship '<${identifier.type}>.${name}' on ${identifier.lid} to be saved as ${inFlightData.data ? inFlightData.data.lid : '<null>'} but it was saved as ${actualData ? actualData.lid : '<null>'}`);
|
|
3154
|
-
}
|
|
3155
|
-
})(inFlightData.data === actualData) : {};
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
});
|
|
3159
|
-
cached.inflightRelationships = null;
|
|
3160
|
-
}
|
|
3161
|
-
}
|
|
3162
1824
|
setupRelationships(cache.__graph, fields, identifier, data);
|
|
3163
1825
|
}
|
|
3164
1826
|
newCanonicalAttributes = data.attributes;
|
|
@@ -3205,21 +1867,6 @@ function willCommit(cache, identifier) {
|
|
|
3205
1867
|
cached.inflightAttrs = cached.localAttrs;
|
|
3206
1868
|
}
|
|
3207
1869
|
cached.localAttrs = null;
|
|
3208
|
-
if (macroCondition(getGlobalConfig().WarpDriveMirror.env.DEBUG)) {
|
|
3209
|
-
if (macroCondition(!getGlobalConfig().WarpDriveMirror.deprecations.DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE)) {
|
|
3210
|
-
// save off info about saved relationships
|
|
3211
|
-
const fields = getCacheFields(cache, identifier);
|
|
3212
|
-
fields.forEach((schema, name) => {
|
|
3213
|
-
if (schema.kind === 'belongsTo') {
|
|
3214
|
-
if (cache.__graph._isDirty(identifier, name)) {
|
|
3215
|
-
const relationshipData = cache.__graph.getData(identifier, name);
|
|
3216
|
-
const inFlight = cached.inflightRelationships = cached.inflightRelationships || Object.create(null);
|
|
3217
|
-
inFlight[name] = relationshipData;
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
});
|
|
3221
|
-
}
|
|
3222
|
-
}
|
|
3223
1870
|
}
|
|
3224
1871
|
|
|
3225
1872
|
export { JSONAPICache };
|