relay-runtime 18.1.0 → 19.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/experimental.js +1 -1
- package/experimental.js.flow +22 -9
- package/handlers/connection/ConnectionHandler.js.flow +6 -1
- package/index.js +1 -1
- package/index.js.flow +4 -0
- package/lib/experimental.js +5 -2
- package/lib/handlers/connection/ConnectionHandler.js +1 -1
- package/lib/index.js +3 -0
- package/lib/multi-actor-environment/ActorSpecificEnvironment.js +1 -1
- package/lib/mutations/RelayRecordProxy.js +14 -3
- package/lib/mutations/RelayRecordSourceMutator.js +17 -0
- package/lib/mutations/RelayRecordSourceProxy.js +2 -1
- package/lib/mutations/createUpdatableProxy.js +1 -1
- package/lib/mutations/validateMutation.js +2 -2
- package/lib/network/RelayObservable.js +1 -3
- package/lib/network/wrapNetworkWithLogObserver.js +2 -2
- package/lib/query/fetchQuery.js +1 -1
- package/lib/store/DataChecker.js +4 -5
- package/lib/store/OperationExecutor.js +11 -0
- package/lib/store/RelayModernEnvironment.js +13 -4
- package/lib/store/RelayModernFragmentSpecResolver.js +4 -4
- package/lib/store/RelayModernStore.js +43 -21
- package/lib/store/RelayPublishQueue.js +11 -15
- package/lib/store/RelayReader.js +244 -183
- package/lib/store/RelayReferenceMarker.js +3 -4
- package/lib/store/RelayResponseNormalizer.js +48 -26
- package/lib/store/RelayStoreSubscriptions.js +2 -2
- package/lib/store/RelayStoreUtils.js +8 -0
- package/lib/store/ResolverCache.js +1 -165
- package/lib/store/ResolverFragments.js +2 -2
- package/lib/store/createRelayLoggingContext.js +17 -0
- package/lib/store/generateTypenamePrefixedDataID.js +9 -0
- package/lib/store/live-resolvers/LiveResolverCache.js +5 -10
- package/lib/store/live-resolvers/resolverDataInjector.js +4 -4
- package/lib/store/observeFragmentExperimental.js +60 -13
- package/lib/store/observeQueryExperimental.js +21 -0
- package/lib/util/RelayFeatureFlags.js +7 -2
- package/lib/util/handlePotentialSnapshotErrors.js +12 -9
- package/multi-actor-environment/ActorSpecificEnvironment.js.flow +1 -0
- package/mutations/RelayRecordProxy.js.flow +30 -3
- package/mutations/RelayRecordSourceMutator.js.flow +27 -0
- package/mutations/RelayRecordSourceProxy.js.flow +4 -0
- package/mutations/createUpdatableProxy.js.flow +1 -1
- package/mutations/validateMutation.js.flow +3 -3
- package/network/RelayNetworkTypes.js.flow +3 -0
- package/network/RelayObservable.js.flow +1 -5
- package/network/wrapNetworkWithLogObserver.js.flow +19 -1
- package/package.json +1 -1
- package/query/fetchQuery.js.flow +1 -1
- package/store/DataChecker.js.flow +5 -2
- package/store/OperationExecutor.js.flow +12 -1
- package/store/RelayExperimentalGraphResponseTransform.js.flow +4 -4
- package/store/RelayModernEnvironment.js.flow +22 -6
- package/store/RelayModernFragmentSpecResolver.js.flow +6 -6
- package/store/RelayModernRecord.js.flow +1 -1
- package/store/RelayModernSelector.js.flow +2 -0
- package/store/RelayModernStore.js.flow +74 -27
- package/store/RelayOptimisticRecordSource.js.flow +2 -0
- package/store/RelayPublishQueue.js.flow +32 -21
- package/store/RelayReader.js.flow +400 -145
- package/store/RelayRecordState.js.flow +1 -1
- package/store/RelayReferenceMarker.js.flow +3 -4
- package/store/RelayResponseNormalizer.js.flow +94 -62
- package/store/RelayStoreSubscriptions.js.flow +2 -2
- package/store/RelayStoreTypes.js.flow +45 -15
- package/store/RelayStoreUtils.js.flow +30 -1
- package/store/ResolverCache.js.flow +2 -271
- package/store/ResolverFragments.js.flow +5 -3
- package/store/StoreInspector.js.flow +5 -0
- package/store/createRelayContext.js.flow +3 -2
- package/store/createRelayLoggingContext.js.flow +46 -0
- package/store/generateTypenamePrefixedDataID.js.flow +25 -0
- package/store/live-resolvers/LiveResolverCache.js.flow +5 -10
- package/store/live-resolvers/resolverDataInjector.js.flow +10 -6
- package/store/observeFragmentExperimental.js.flow +82 -28
- package/store/observeQueryExperimental.js.flow +61 -0
- package/store/waitForFragmentExperimental.js.flow +4 -3
- package/util/NormalizationNode.js.flow +10 -1
- package/util/ReaderNode.js.flow +9 -3
- package/util/RelayConcreteNode.js.flow +3 -1
- package/util/RelayError.js.flow +1 -0
- package/util/RelayFeatureFlags.js.flow +31 -7
- package/util/RelayRuntimeTypes.js.flow +17 -3
- package/util/getPaginationVariables.js.flow +2 -0
- package/util/handlePotentialSnapshotErrors.js.flow +24 -12
- package/util/registerEnvironmentWithDevTools.js.flow +4 -2
- package/util/withProvidedVariables.js.flow +1 -0
- package/util/withStartAndDuration.js.flow +3 -0
- package/relay-runtime-experimental.js +0 -4
- package/relay-runtime-experimental.min.js +0 -9
- package/relay-runtime.js +0 -4
- package/relay-runtime.min.js +0 -9
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
+
import type {Result} from '../experimental';
|
|
14
15
|
import type {
|
|
16
|
+
CatchFieldTo,
|
|
15
17
|
ReaderActorChange,
|
|
18
|
+
ReaderAliasedInlineFragmentSpread,
|
|
16
19
|
ReaderCatchField,
|
|
17
20
|
ReaderClientEdge,
|
|
18
21
|
ReaderFragment,
|
|
@@ -32,10 +35,9 @@ import type {DataID, Variables} from '../util/RelayRuntimeTypes';
|
|
|
32
35
|
import type {
|
|
33
36
|
ClientEdgeTraversalInfo,
|
|
34
37
|
DataIDSet,
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
FieldError,
|
|
39
|
+
FieldErrors,
|
|
37
40
|
MissingClientEdgeRequestInfo,
|
|
38
|
-
MissingLiveResolverField,
|
|
39
41
|
Record,
|
|
40
42
|
RecordSource,
|
|
41
43
|
RequestDescriptor,
|
|
@@ -47,6 +49,7 @@ import type {
|
|
|
47
49
|
import type {Arguments} from './RelayStoreUtils';
|
|
48
50
|
import type {EvaluationResult, ResolverCache} from './ResolverCache';
|
|
49
51
|
|
|
52
|
+
const RelayFeatureFlags = require('../util/RelayFeatureFlags');
|
|
50
53
|
const {
|
|
51
54
|
isSuspenseSentinel,
|
|
52
55
|
} = require('./live-resolvers/LiveResolverSuspenseSentinel');
|
|
@@ -72,8 +75,6 @@ const {
|
|
|
72
75
|
const {generateTypeID} = require('./TypeID');
|
|
73
76
|
const invariant = require('invariant');
|
|
74
77
|
|
|
75
|
-
type RequiredOrCatchField = ReaderRequiredField | ReaderCatchField;
|
|
76
|
-
|
|
77
78
|
function read(
|
|
78
79
|
recordSource: RecordSource,
|
|
79
80
|
selector: SingularReaderSelector,
|
|
@@ -96,10 +97,16 @@ class RelayReader {
|
|
|
96
97
|
_clientEdgeTraversalPath: Array<ClientEdgeTraversalInfo | null>;
|
|
97
98
|
_isMissingData: boolean;
|
|
98
99
|
_missingClientEdges: Array<MissingClientEdgeRequestInfo>;
|
|
99
|
-
_missingLiveResolverFields: Array<
|
|
100
|
+
_missingLiveResolverFields: Array<DataID>;
|
|
100
101
|
_isWithinUnmatchedTypeRefinement: boolean;
|
|
101
|
-
|
|
102
|
+
_fieldErrors: ?FieldErrors;
|
|
102
103
|
_owner: RequestDescriptor;
|
|
104
|
+
// Exec time resolvers are run before reaching the Relay store so the store already contains
|
|
105
|
+
// the normalized data; the same as if the data were sent from the server. However, since a
|
|
106
|
+
// resolver could be used at read time or exec time in different queries, the reader AST for
|
|
107
|
+
// a resolver is the read time AST. At runtime, this flag is used to ignore the extra
|
|
108
|
+
// information in the read time resolver AST and use the "standard", non-resolver read paths
|
|
109
|
+
_useExecTimeResolvers: boolean;
|
|
103
110
|
_recordSource: RecordSource;
|
|
104
111
|
_seenRecords: DataIDSet;
|
|
105
112
|
_updatedDataIDs: DataIDSet;
|
|
@@ -122,8 +129,13 @@ class RelayReader {
|
|
|
122
129
|
this._missingLiveResolverFields = [];
|
|
123
130
|
this._isMissingData = false;
|
|
124
131
|
this._isWithinUnmatchedTypeRefinement = false;
|
|
125
|
-
this.
|
|
132
|
+
this._fieldErrors = null;
|
|
126
133
|
this._owner = selector.owner;
|
|
134
|
+
this._useExecTimeResolvers =
|
|
135
|
+
this._owner.node.operation.use_exec_time_resolvers ??
|
|
136
|
+
this._owner.node.operation.exec_time_resolvers_enabled_provider?.get() ===
|
|
137
|
+
true ??
|
|
138
|
+
false;
|
|
127
139
|
this._recordSource = recordSource;
|
|
128
140
|
this._seenRecords = new Set();
|
|
129
141
|
this._selector = selector;
|
|
@@ -175,7 +187,14 @@ class RelayReader {
|
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
this._isWithinUnmatchedTypeRefinement = !isDataExpectedToBePresent;
|
|
178
|
-
|
|
190
|
+
let data = this._traverse(node, dataID, null);
|
|
191
|
+
|
|
192
|
+
// If the fragment/operation was marked with @catch, we need to handle any
|
|
193
|
+
// errors that were encountered while reading the fields within it.
|
|
194
|
+
const catchTo = this._selector.node.metadata?.catchTo;
|
|
195
|
+
if (catchTo != null) {
|
|
196
|
+
data = this._catchErrors(data, catchTo, null) as $FlowFixMe;
|
|
197
|
+
}
|
|
179
198
|
|
|
180
199
|
if (this._updatedDataIDs.size > 0) {
|
|
181
200
|
this._resolverCache.notifyUpdatedSubscribers(this._updatedDataIDs);
|
|
@@ -190,11 +209,11 @@ class RelayReader {
|
|
|
190
209
|
missingLiveResolverFields: this._missingLiveResolverFields,
|
|
191
210
|
seenRecords: this._seenRecords,
|
|
192
211
|
selector: this._selector,
|
|
193
|
-
|
|
212
|
+
fieldErrors: this._fieldErrors,
|
|
194
213
|
};
|
|
195
214
|
}
|
|
196
215
|
|
|
197
|
-
|
|
216
|
+
_maybeAddFieldErrors(record: Record, storageKey: string): void {
|
|
198
217
|
const errors = RelayModernRecord.getErrors(record, storageKey);
|
|
199
218
|
|
|
200
219
|
if (errors == null) {
|
|
@@ -202,42 +221,55 @@ class RelayReader {
|
|
|
202
221
|
}
|
|
203
222
|
const owner = this._fragmentName;
|
|
204
223
|
|
|
205
|
-
if (this.
|
|
206
|
-
this.
|
|
224
|
+
if (this._fieldErrors == null) {
|
|
225
|
+
this._fieldErrors = [];
|
|
207
226
|
}
|
|
208
|
-
for (
|
|
209
|
-
|
|
227
|
+
for (let i = 0; i < errors.length; i++) {
|
|
228
|
+
const error = errors[i];
|
|
229
|
+
this._fieldErrors.push({
|
|
210
230
|
kind: 'relay_field_payload.error',
|
|
211
231
|
owner,
|
|
212
232
|
fieldPath: (error.path ?? []).join('.'),
|
|
213
233
|
error,
|
|
214
234
|
shouldThrow: this._selector.node.metadata?.throwOnFieldError ?? false,
|
|
215
235
|
handled: false,
|
|
236
|
+
// the uiContext is always undefined here.
|
|
237
|
+
// the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
|
|
238
|
+
uiContext: undefined,
|
|
216
239
|
});
|
|
217
240
|
}
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
_markDataAsMissing(): void {
|
|
243
|
+
_markDataAsMissing(fieldName: string): void {
|
|
221
244
|
if (this._isWithinUnmatchedTypeRefinement) {
|
|
222
245
|
return;
|
|
223
246
|
}
|
|
224
|
-
if (this.
|
|
225
|
-
this.
|
|
247
|
+
if (this._fieldErrors == null) {
|
|
248
|
+
this._fieldErrors = [];
|
|
226
249
|
}
|
|
227
250
|
|
|
228
251
|
// we will add the path later
|
|
229
|
-
const fieldPath = '';
|
|
230
252
|
const owner = this._fragmentName;
|
|
231
253
|
|
|
232
|
-
this.
|
|
254
|
+
this._fieldErrors.push(
|
|
233
255
|
this._selector.node.metadata?.throwOnFieldError ?? false
|
|
234
256
|
? {
|
|
235
257
|
kind: 'missing_expected_data.throw',
|
|
236
258
|
owner,
|
|
237
|
-
fieldPath,
|
|
259
|
+
fieldPath: fieldName,
|
|
238
260
|
handled: false,
|
|
261
|
+
// the uiContext is always undefined here.
|
|
262
|
+
// the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
|
|
263
|
+
uiContext: undefined,
|
|
239
264
|
}
|
|
240
|
-
: {
|
|
265
|
+
: {
|
|
266
|
+
kind: 'missing_expected_data.log',
|
|
267
|
+
owner,
|
|
268
|
+
fieldPath: fieldName,
|
|
269
|
+
// the uiContext is always undefined here.
|
|
270
|
+
// the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
|
|
271
|
+
uiContext: undefined,
|
|
272
|
+
},
|
|
241
273
|
);
|
|
242
274
|
|
|
243
275
|
this._isMissingData = true;
|
|
@@ -266,8 +298,9 @@ class RelayReader {
|
|
|
266
298
|
this._seenRecords.add(dataID);
|
|
267
299
|
if (record == null) {
|
|
268
300
|
if (record === undefined) {
|
|
269
|
-
this._markDataAsMissing();
|
|
301
|
+
this._markDataAsMissing('<record>');
|
|
270
302
|
}
|
|
303
|
+
// $FlowFixMe[incompatible-return]
|
|
271
304
|
return record;
|
|
272
305
|
}
|
|
273
306
|
const data = prevData || {};
|
|
@@ -288,53 +321,139 @@ class RelayReader {
|
|
|
288
321
|
return this._variables[name];
|
|
289
322
|
}
|
|
290
323
|
|
|
291
|
-
_maybeReportUnexpectedNull(
|
|
324
|
+
_maybeReportUnexpectedNull(selection: ReaderRequiredField) {
|
|
325
|
+
if (selection.action === 'NONE') {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
292
328
|
const owner = this._fragmentName;
|
|
293
329
|
|
|
294
|
-
if (this.
|
|
295
|
-
this.
|
|
330
|
+
if (this._fieldErrors == null) {
|
|
331
|
+
this._fieldErrors = [];
|
|
296
332
|
}
|
|
297
333
|
|
|
298
|
-
|
|
334
|
+
let fieldName: string;
|
|
335
|
+
if (selection.field.linkedField != null) {
|
|
336
|
+
fieldName =
|
|
337
|
+
selection.field.linkedField.alias ?? selection.field.linkedField.name;
|
|
338
|
+
} else {
|
|
339
|
+
fieldName = selection.field.alias ?? selection.field.name;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
switch (selection.action) {
|
|
299
343
|
case 'THROW':
|
|
300
|
-
this.
|
|
344
|
+
this._fieldErrors.push({
|
|
301
345
|
kind: 'missing_required_field.throw',
|
|
302
|
-
fieldPath,
|
|
346
|
+
fieldPath: fieldName,
|
|
303
347
|
owner,
|
|
304
348
|
handled: false,
|
|
349
|
+
// the uiContext is always undefined here.
|
|
350
|
+
// the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
|
|
351
|
+
uiContext: undefined,
|
|
305
352
|
});
|
|
306
353
|
return;
|
|
307
354
|
case 'LOG':
|
|
308
|
-
this.
|
|
355
|
+
this._fieldErrors.push({
|
|
309
356
|
kind: 'missing_required_field.log',
|
|
310
|
-
fieldPath,
|
|
357
|
+
fieldPath: fieldName,
|
|
311
358
|
owner,
|
|
359
|
+
// the uiContext is always undefined here.
|
|
360
|
+
// the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
|
|
361
|
+
uiContext: undefined,
|
|
312
362
|
});
|
|
313
363
|
return;
|
|
314
364
|
default:
|
|
315
|
-
(action: empty);
|
|
365
|
+
(selection.action: empty);
|
|
316
366
|
}
|
|
317
367
|
}
|
|
318
368
|
|
|
319
|
-
|
|
320
|
-
selection:
|
|
321
|
-
record: Record,
|
|
322
|
-
data: SelectorData,
|
|
369
|
+
_handleRequiredFieldValue(
|
|
370
|
+
selection: ReaderRequiredField,
|
|
323
371
|
value: mixed,
|
|
324
|
-
) {
|
|
325
|
-
|
|
326
|
-
|
|
372
|
+
): boolean /*should continue to siblings*/ {
|
|
373
|
+
if (value == null) {
|
|
374
|
+
this._maybeReportUnexpectedNull(selection);
|
|
375
|
+
// We are going to throw, or our parent is going to get nulled out.
|
|
376
|
+
// Either way, sibling values are going to be ignored, so we can
|
|
377
|
+
// bail early here as an optimization.
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
327
382
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Fields, aliased inline fragments, fragments and operations with `@catch`
|
|
385
|
+
* directives must handle the case that errors were encountered while reading
|
|
386
|
+
* any fields within them.
|
|
387
|
+
*
|
|
388
|
+
* 1. Before traversing into the selection(s) marked as `@catch`, the caller
|
|
389
|
+
* stores the previous field errors (`this._fieldErrors`) in a
|
|
390
|
+
* variable.
|
|
391
|
+
* 2. After traversing into the selection(s) marked as `@catch`, the caller
|
|
392
|
+
* calls this method with the resulting value, the `to` value from the
|
|
393
|
+
* `@catch` directive, and the previous field errors.
|
|
394
|
+
*
|
|
395
|
+
* This method will then:
|
|
396
|
+
*
|
|
397
|
+
* 1. Compute the correct value to return based on any errors encountered and the supplied `to` type.
|
|
398
|
+
* 2. Mark any errors encountered within the `@catch` as "handled" to ensure they don't cause the reader to throw.
|
|
399
|
+
* 3. Merge any errors encountered within the `@catch` with the previous field errors.
|
|
400
|
+
*/
|
|
401
|
+
_catchErrors<T>(
|
|
402
|
+
_value: T,
|
|
403
|
+
to: CatchFieldTo,
|
|
404
|
+
previousResponseFields: ?FieldErrors,
|
|
405
|
+
): ?T | Result<T, mixed> {
|
|
406
|
+
let value: T | null | Result<T, mixed> = _value;
|
|
407
|
+
switch (to) {
|
|
408
|
+
case 'RESULT':
|
|
409
|
+
value = this._asResult(_value);
|
|
410
|
+
break;
|
|
411
|
+
case 'NULL':
|
|
412
|
+
if (this._fieldErrors != null && this._fieldErrors.length > 0) {
|
|
413
|
+
value = null;
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
default:
|
|
417
|
+
(to: empty);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const childrenFieldErrors = this._fieldErrors;
|
|
421
|
+
|
|
422
|
+
this._fieldErrors = previousResponseFields;
|
|
423
|
+
|
|
424
|
+
// Merge any errors encountered within the @catch with the previous field
|
|
425
|
+
// errors, but mark them as "handled" first.
|
|
426
|
+
if (childrenFieldErrors != null) {
|
|
427
|
+
if (this._fieldErrors == null) {
|
|
428
|
+
this._fieldErrors = [];
|
|
429
|
+
}
|
|
430
|
+
for (let i = 0; i < childrenFieldErrors.length; i++) {
|
|
431
|
+
// We mark any errors encountered within the @catch as "handled"
|
|
432
|
+
// to ensure that they don't cause the reader to throw, but can
|
|
433
|
+
// still be logged.
|
|
434
|
+
this._fieldErrors.push(
|
|
435
|
+
markFieldErrorHasHandled(childrenFieldErrors[i]),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Convert a value into a Result object based on the presence of errors in the
|
|
444
|
+
* `this._fieldErrors` array.
|
|
445
|
+
*
|
|
446
|
+
* **Note**: This method does _not_ mark errors as handled. It is the caller's
|
|
447
|
+
* responsibility to ensure that errors are marked as handled.
|
|
448
|
+
*/
|
|
449
|
+
_asResult<T>(value: T): Result<T, mixed> {
|
|
450
|
+
if (this._fieldErrors == null || this._fieldErrors.length === 0) {
|
|
451
|
+
return {ok: true, value};
|
|
452
|
+
}
|
|
334
453
|
|
|
335
454
|
// TODO: Should we be hiding log level events here?
|
|
336
|
-
const errors = this.
|
|
337
|
-
|
|
455
|
+
const errors = this._fieldErrors
|
|
456
|
+
.map(error => {
|
|
338
457
|
switch (error.kind) {
|
|
339
458
|
case 'relay_field_payload.error':
|
|
340
459
|
const {message, ...displayError} = error.error;
|
|
@@ -361,31 +480,14 @@ class RelayReader {
|
|
|
361
480
|
(error.kind: empty);
|
|
362
481
|
invariant(
|
|
363
482
|
false,
|
|
364
|
-
'Unexpected error
|
|
483
|
+
'Unexpected error fieldError kind: %s',
|
|
365
484
|
error.kind,
|
|
366
485
|
);
|
|
367
486
|
}
|
|
368
487
|
})
|
|
369
488
|
.filter(Boolean);
|
|
370
489
|
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
_handleRequiredFieldValue(
|
|
375
|
-
selection: ReaderRequiredField,
|
|
376
|
-
value: mixed,
|
|
377
|
-
): boolean /*should continue to siblings*/ {
|
|
378
|
-
if (value == null) {
|
|
379
|
-
const {action} = selection;
|
|
380
|
-
if (action !== 'NONE') {
|
|
381
|
-
this._maybeReportUnexpectedNull(selection.path, action);
|
|
382
|
-
}
|
|
383
|
-
// We are going to throw, or our parent is going to get nulled out.
|
|
384
|
-
// Either way, sibling values are going to be ignored, so we can
|
|
385
|
-
// bail early here as an optimization.
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
return true;
|
|
490
|
+
return {ok: false, errors};
|
|
389
491
|
}
|
|
390
492
|
|
|
391
493
|
_traverseSelections(
|
|
@@ -408,9 +510,9 @@ class RelayReader {
|
|
|
408
510
|
}
|
|
409
511
|
break;
|
|
410
512
|
case 'CatchField': {
|
|
411
|
-
const previousResponseFields = this.
|
|
513
|
+
const previousResponseFields = this._fieldErrors;
|
|
412
514
|
|
|
413
|
-
this.
|
|
515
|
+
this._fieldErrors = null;
|
|
414
516
|
|
|
415
517
|
const catchFieldValue = this._readClientSideDirectiveField(
|
|
416
518
|
selection,
|
|
@@ -418,27 +520,20 @@ class RelayReader {
|
|
|
418
520
|
data,
|
|
419
521
|
);
|
|
420
522
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
523
|
+
const field = selection.field?.backingField ?? selection.field;
|
|
524
|
+
const fieldName = field?.alias ?? field?.name;
|
|
525
|
+
// ReaderClientExtension doesn't have `alias` or `name`
|
|
526
|
+
// so we don't support this yet
|
|
527
|
+
invariant(
|
|
528
|
+
fieldName != null,
|
|
529
|
+
"Couldn't determine field name for this field. It might be a ReaderClientExtension - which is not yet supported.",
|
|
530
|
+
);
|
|
428
531
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// We mark any errors encountered within the @catch as "handled"
|
|
435
|
-
// to ensure that they don't cause the reader to throw, but can
|
|
436
|
-
// still be logged.
|
|
437
|
-
this._errorResponseFields.push(
|
|
438
|
-
markFieldErrorHasHandled(childrenErrorResponseFields[i]),
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
532
|
+
data[fieldName] = this._catchErrors(
|
|
533
|
+
catchFieldValue,
|
|
534
|
+
selection.to,
|
|
535
|
+
previousResponseFields,
|
|
536
|
+
);
|
|
442
537
|
|
|
443
538
|
break;
|
|
444
539
|
}
|
|
@@ -482,23 +577,18 @@ class RelayReader {
|
|
|
482
577
|
}
|
|
483
578
|
case 'RelayLiveResolver':
|
|
484
579
|
case 'RelayResolver': {
|
|
485
|
-
this.
|
|
580
|
+
if (this._useExecTimeResolvers) {
|
|
581
|
+
this._readScalar(selection, record, data);
|
|
582
|
+
} else {
|
|
583
|
+
this._readResolverField(selection, record, data);
|
|
584
|
+
}
|
|
486
585
|
break;
|
|
487
586
|
}
|
|
488
587
|
case 'FragmentSpread':
|
|
489
588
|
this._createFragmentPointer(selection, record, data);
|
|
490
589
|
break;
|
|
491
590
|
case 'AliasedInlineFragmentSpread': {
|
|
492
|
-
|
|
493
|
-
selection.fragment,
|
|
494
|
-
record,
|
|
495
|
-
{},
|
|
496
|
-
true,
|
|
497
|
-
);
|
|
498
|
-
if (fieldValue === false) {
|
|
499
|
-
fieldValue = null;
|
|
500
|
-
}
|
|
501
|
-
data[selection.name] = fieldValue;
|
|
591
|
+
this._readAliasedInlineFragment(selection, record, data);
|
|
502
592
|
break;
|
|
503
593
|
}
|
|
504
594
|
case 'ModuleImport':
|
|
@@ -550,7 +640,20 @@ class RelayReader {
|
|
|
550
640
|
break;
|
|
551
641
|
case 'ClientEdgeToClientObject':
|
|
552
642
|
case 'ClientEdgeToServerObject':
|
|
553
|
-
|
|
643
|
+
if (
|
|
644
|
+
this._useExecTimeResolvers &&
|
|
645
|
+
(selection.backingField.kind === 'RelayResolver' ||
|
|
646
|
+
selection.backingField.kind === 'RelayLiveResolver')
|
|
647
|
+
) {
|
|
648
|
+
const {linkedField} = selection;
|
|
649
|
+
if (linkedField.plural) {
|
|
650
|
+
this._readPluralLink(linkedField, record, data);
|
|
651
|
+
} else {
|
|
652
|
+
this._readLink(linkedField, record, data);
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
this._readClientEdge(selection, record, data);
|
|
656
|
+
}
|
|
554
657
|
break;
|
|
555
658
|
default:
|
|
556
659
|
(selection: empty);
|
|
@@ -565,7 +668,7 @@ class RelayReader {
|
|
|
565
668
|
}
|
|
566
669
|
|
|
567
670
|
_readClientSideDirectiveField(
|
|
568
|
-
selection:
|
|
671
|
+
selection: ReaderRequiredField | ReaderCatchField,
|
|
569
672
|
record: Record,
|
|
570
673
|
data: SelectorData,
|
|
571
674
|
): ?mixed {
|
|
@@ -578,19 +681,39 @@ class RelayReader {
|
|
|
578
681
|
} else {
|
|
579
682
|
return this._readLink(selection.field, record, data);
|
|
580
683
|
}
|
|
684
|
+
|
|
581
685
|
case 'RelayResolver':
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
686
|
+
case 'RelayLiveResolver': {
|
|
687
|
+
if (this._useExecTimeResolvers) {
|
|
688
|
+
return this._readScalar(selection.field, record, data);
|
|
689
|
+
} else {
|
|
690
|
+
return this._readResolverField(selection.field, record, data);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
585
693
|
case 'ClientEdgeToClientObject':
|
|
586
694
|
case 'ClientEdgeToServerObject':
|
|
587
|
-
|
|
695
|
+
if (
|
|
696
|
+
this._useExecTimeResolvers &&
|
|
697
|
+
(selection.field.backingField.kind === 'RelayResolver' ||
|
|
698
|
+
selection.field.backingField.kind === 'RelayLiveResolver')
|
|
699
|
+
) {
|
|
700
|
+
const {field} = selection;
|
|
701
|
+
if (field.linkedField.plural) {
|
|
702
|
+
return this._readPluralLink(field.linkedField, record, data);
|
|
703
|
+
} else {
|
|
704
|
+
return this._readLink(field.linkedField, record, data);
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
return this._readClientEdge(selection.field, record, data);
|
|
708
|
+
}
|
|
709
|
+
case 'AliasedInlineFragmentSpread':
|
|
710
|
+
return this._readAliasedInlineFragment(selection.field, record, data);
|
|
588
711
|
default:
|
|
589
712
|
(selection.field.kind: empty);
|
|
590
713
|
invariant(
|
|
591
714
|
false,
|
|
592
715
|
'RelayReader(): Unexpected ast kind `%s`.',
|
|
593
|
-
selection.kind,
|
|
716
|
+
selection.field.kind,
|
|
594
717
|
);
|
|
595
718
|
}
|
|
596
719
|
}
|
|
@@ -601,9 +724,12 @@ class RelayReader {
|
|
|
601
724
|
data: SelectorData,
|
|
602
725
|
): mixed {
|
|
603
726
|
const parentRecordID = RelayModernRecord.getDataID(record);
|
|
727
|
+
const prevErrors = this._fieldErrors;
|
|
728
|
+
this._fieldErrors = null;
|
|
604
729
|
const result = this._readResolverFieldImpl(field, parentRecordID);
|
|
605
730
|
|
|
606
731
|
const fieldName = field.alias ?? field.name;
|
|
732
|
+
this._prependPreviousErrors(prevErrors, fieldName);
|
|
607
733
|
data[fieldName] = result;
|
|
608
734
|
return result;
|
|
609
735
|
}
|
|
@@ -637,7 +763,7 @@ class RelayReader {
|
|
|
637
763
|
return {
|
|
638
764
|
data: snapshot.data,
|
|
639
765
|
isMissingData: snapshot.isMissingData,
|
|
640
|
-
|
|
766
|
+
fieldErrors: snapshot.fieldErrors,
|
|
641
767
|
};
|
|
642
768
|
}
|
|
643
769
|
|
|
@@ -650,7 +776,7 @@ class RelayReader {
|
|
|
650
776
|
return {
|
|
651
777
|
data: snapshot.data,
|
|
652
778
|
isMissingData: snapshot.isMissingData,
|
|
653
|
-
|
|
779
|
+
fieldErrors: snapshot.fieldErrors,
|
|
654
780
|
};
|
|
655
781
|
};
|
|
656
782
|
|
|
@@ -735,7 +861,8 @@ class RelayReader {
|
|
|
735
861
|
// upwards to mimic the behavior of having traversed into that fragment directly.
|
|
736
862
|
if (cachedSnapshot != null) {
|
|
737
863
|
if (cachedSnapshot.missingClientEdges != null) {
|
|
738
|
-
for (
|
|
864
|
+
for (let i = 0; i < cachedSnapshot.missingClientEdges.length; i++) {
|
|
865
|
+
const missing = cachedSnapshot.missingClientEdges[i];
|
|
739
866
|
this._missingClientEdges.push(missing);
|
|
740
867
|
}
|
|
741
868
|
}
|
|
@@ -744,27 +871,34 @@ class RelayReader {
|
|
|
744
871
|
this._isMissingData ||
|
|
745
872
|
cachedSnapshot.missingLiveResolverFields.length > 0;
|
|
746
873
|
|
|
747
|
-
for (
|
|
874
|
+
for (
|
|
875
|
+
let i = 0;
|
|
876
|
+
i < cachedSnapshot.missingLiveResolverFields.length;
|
|
877
|
+
i++
|
|
878
|
+
) {
|
|
879
|
+
const missingResolverField =
|
|
880
|
+
cachedSnapshot.missingLiveResolverFields[i];
|
|
748
881
|
this._missingLiveResolverFields.push(missingResolverField);
|
|
749
882
|
}
|
|
750
883
|
}
|
|
751
|
-
if (cachedSnapshot.
|
|
752
|
-
if (this.
|
|
753
|
-
this.
|
|
884
|
+
if (cachedSnapshot.fieldErrors != null) {
|
|
885
|
+
if (this._fieldErrors == null) {
|
|
886
|
+
this._fieldErrors = [];
|
|
754
887
|
}
|
|
755
|
-
for (
|
|
888
|
+
for (let i = 0; i < cachedSnapshot.fieldErrors.length; i++) {
|
|
889
|
+
const error = cachedSnapshot.fieldErrors[i];
|
|
756
890
|
if (this._selector.node.metadata?.throwOnFieldError === true) {
|
|
757
891
|
// If this fragment is @throwOnFieldError, any destructive error
|
|
758
892
|
// encountered inside a resolver's fragment is equivilent to the
|
|
759
893
|
// resolver field having a field error, and we want that to cause this
|
|
760
894
|
// fragment to throw. So, we propagate all errors as is.
|
|
761
|
-
this.
|
|
895
|
+
this._fieldErrors.push(error);
|
|
762
896
|
} else {
|
|
763
897
|
// If this fragment is _not_ @throwOnFieldError, we will simply
|
|
764
898
|
// accept that any destructive errors encountered in the resolver's
|
|
765
899
|
// root fragment will cause the resolver to return null, and well
|
|
766
900
|
// pass the errors along to the logger marked as "handled".
|
|
767
|
-
this.
|
|
901
|
+
this._fieldErrors.push(markFieldErrorHasHandled(error));
|
|
768
902
|
}
|
|
769
903
|
}
|
|
770
904
|
}
|
|
@@ -775,18 +909,21 @@ class RelayReader {
|
|
|
775
909
|
// the errors can be attached to this read's snapshot. This allows the error
|
|
776
910
|
// to be logged.
|
|
777
911
|
if (resolverError) {
|
|
778
|
-
const errorEvent = {
|
|
912
|
+
const errorEvent: FieldError = {
|
|
779
913
|
kind: 'relay_resolver.error',
|
|
780
914
|
fieldPath,
|
|
781
915
|
owner: this._fragmentName,
|
|
782
916
|
error: resolverError,
|
|
783
917
|
shouldThrow: this._selector.node.metadata?.throwOnFieldError ?? false,
|
|
784
918
|
handled: false,
|
|
919
|
+
// the uiContext is always undefined here.
|
|
920
|
+
// the loggingContext is provided by hooks - and assigned to uiContext in handlePotentialSnapshotErrors
|
|
921
|
+
uiContext: undefined,
|
|
785
922
|
};
|
|
786
|
-
if (this.
|
|
787
|
-
this.
|
|
923
|
+
if (this._fieldErrors == null) {
|
|
924
|
+
this._fieldErrors = [errorEvent];
|
|
788
925
|
} else {
|
|
789
|
-
this.
|
|
926
|
+
this._fieldErrors.push(errorEvent);
|
|
790
927
|
}
|
|
791
928
|
}
|
|
792
929
|
|
|
@@ -804,15 +941,14 @@ class RelayReader {
|
|
|
804
941
|
// they know when to unsuspend.
|
|
805
942
|
if (suspenseID != null) {
|
|
806
943
|
this._isMissingData = true;
|
|
807
|
-
this._missingLiveResolverFields.push(
|
|
808
|
-
path: `${this._fragmentName}.${fieldPath}`,
|
|
809
|
-
liveStateID: suspenseID,
|
|
810
|
-
});
|
|
944
|
+
this._missingLiveResolverFields.push(suspenseID);
|
|
811
945
|
}
|
|
812
946
|
if (updatedDataIDs != null) {
|
|
813
|
-
|
|
947
|
+
// Iterating a Set with for of is okay
|
|
948
|
+
// eslint-disable-next-line relay-internal/no-for-of-loops
|
|
949
|
+
updatedDataIDs.forEach(recordID => {
|
|
814
950
|
this._updatedDataIDs.add(recordID);
|
|
815
|
-
}
|
|
951
|
+
});
|
|
816
952
|
}
|
|
817
953
|
}
|
|
818
954
|
|
|
@@ -982,12 +1118,15 @@ class RelayReader {
|
|
|
982
1118
|
RelayModernRecord.getDataID(record),
|
|
983
1119
|
prevData,
|
|
984
1120
|
);
|
|
1121
|
+
const prevErrors = this._fieldErrors;
|
|
1122
|
+
this._fieldErrors = null;
|
|
985
1123
|
const edgeValue = this._traverse(
|
|
986
1124
|
field.linkedField,
|
|
987
1125
|
storeID,
|
|
988
1126
|
// $FlowFixMe[incompatible-variance]
|
|
989
1127
|
prevData,
|
|
990
1128
|
);
|
|
1129
|
+
this._prependPreviousErrors(prevErrors, fieldName);
|
|
991
1130
|
this._clientEdgeTraversalPath.pop();
|
|
992
1131
|
data[fieldName] = edgeValue;
|
|
993
1132
|
return edgeValue;
|
|
@@ -995,17 +1134,22 @@ class RelayReader {
|
|
|
995
1134
|
}
|
|
996
1135
|
|
|
997
1136
|
_readScalar(
|
|
998
|
-
field: ReaderScalarField,
|
|
1137
|
+
field: ReaderScalarField | ReaderRelayResolver | ReaderRelayLiveResolver,
|
|
999
1138
|
record: Record,
|
|
1000
1139
|
data: SelectorData,
|
|
1001
1140
|
): ?mixed {
|
|
1002
1141
|
const fieldName = field.alias ?? field.name;
|
|
1003
1142
|
const storageKey = getStorageKey(field, this._variables);
|
|
1004
1143
|
const value = RelayModernRecord.getValue(record, storageKey);
|
|
1005
|
-
if (
|
|
1006
|
-
|
|
1144
|
+
if (
|
|
1145
|
+
value === null ||
|
|
1146
|
+
(RelayFeatureFlags.ENABLE_NONCOMPLIANT_ERROR_HANDLING_ON_LISTS &&
|
|
1147
|
+
Array.isArray(value) &&
|
|
1148
|
+
value.length === 0)
|
|
1149
|
+
) {
|
|
1150
|
+
this._maybeAddFieldErrors(record, storageKey);
|
|
1007
1151
|
} else if (value === undefined) {
|
|
1008
|
-
this._markDataAsMissing();
|
|
1152
|
+
this._markDataAsMissing(fieldName);
|
|
1009
1153
|
}
|
|
1010
1154
|
data[fieldName] = value;
|
|
1011
1155
|
return value;
|
|
@@ -1022,9 +1166,9 @@ class RelayReader {
|
|
|
1022
1166
|
if (linkedID == null) {
|
|
1023
1167
|
data[fieldName] = linkedID;
|
|
1024
1168
|
if (linkedID === null) {
|
|
1025
|
-
this.
|
|
1169
|
+
this._maybeAddFieldErrors(record, storageKey);
|
|
1026
1170
|
} else if (linkedID === undefined) {
|
|
1027
|
-
this._markDataAsMissing();
|
|
1171
|
+
this._markDataAsMissing(fieldName);
|
|
1028
1172
|
}
|
|
1029
1173
|
return linkedID;
|
|
1030
1174
|
}
|
|
@@ -1039,12 +1183,76 @@ class RelayReader {
|
|
|
1039
1183
|
RelayModernRecord.getDataID(record),
|
|
1040
1184
|
prevData,
|
|
1041
1185
|
);
|
|
1186
|
+
const prevErrors = this._fieldErrors;
|
|
1187
|
+
this._fieldErrors = null;
|
|
1042
1188
|
// $FlowFixMe[incompatible-variance]
|
|
1043
1189
|
const value = this._traverse(field, linkedID, prevData);
|
|
1190
|
+
|
|
1191
|
+
this._prependPreviousErrors(prevErrors, fieldName);
|
|
1044
1192
|
data[fieldName] = value;
|
|
1045
1193
|
return value;
|
|
1046
1194
|
}
|
|
1047
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* Adds a set of field errors to `this._fieldErrors`, ensuring the
|
|
1198
|
+
* `fieldPath` property of existing field errors are prefixed with the given
|
|
1199
|
+
* `fieldNameOrIndex`.
|
|
1200
|
+
*
|
|
1201
|
+
* In order to make field errors maximally useful in logs/errors, we want to
|
|
1202
|
+
* include the path to the field that caused the error. A naive approach would
|
|
1203
|
+
* be to maintain a path property on RelayReader which we push/pop field names
|
|
1204
|
+
* to as we traverse into fields/etc. However, this would be expensive to
|
|
1205
|
+
* maintain, and in the common case where there are no field errors, the work
|
|
1206
|
+
* would go unused.
|
|
1207
|
+
*
|
|
1208
|
+
* Instead, we take a lazy approach where as we exit the recurison into a
|
|
1209
|
+
* field/etc we prepend any errors encountered while traversing that field
|
|
1210
|
+
* with the field name. This is somewhat more expensive in the error case, but
|
|
1211
|
+
* ~free in the common case where there are no errors.
|
|
1212
|
+
*
|
|
1213
|
+
* To achieve this, named field readers must do the following to correctly
|
|
1214
|
+
* track error filePaths:
|
|
1215
|
+
*
|
|
1216
|
+
* 1. Stash the value of `this._fieldErrors` in a local variable
|
|
1217
|
+
* 2. Set `this._fieldErrors` to `null`
|
|
1218
|
+
* 3. Traverse into the field
|
|
1219
|
+
* 4. Call this method with the stashed errors and the field's name
|
|
1220
|
+
*
|
|
1221
|
+
* Similarly, when creating field errors, we simply initialize the `fieldPath`
|
|
1222
|
+
* as the direct field name.
|
|
1223
|
+
*
|
|
1224
|
+
* Today we only use this apporach for `missing_expected_data` and
|
|
1225
|
+
* `missing_required_field` errors, but we intend to broaden it to handle all
|
|
1226
|
+
* field error paths.
|
|
1227
|
+
*/
|
|
1228
|
+
_prependPreviousErrors(
|
|
1229
|
+
prevErrors: ?Array<FieldError>,
|
|
1230
|
+
fieldNameOrIndex: string | number,
|
|
1231
|
+
): void {
|
|
1232
|
+
if (this._fieldErrors != null) {
|
|
1233
|
+
for (let i = 0; i < this._fieldErrors.length; i++) {
|
|
1234
|
+
const event = this._fieldErrors[i];
|
|
1235
|
+
if (
|
|
1236
|
+
event.owner === this._fragmentName &&
|
|
1237
|
+
(event.kind === 'missing_expected_data.throw' ||
|
|
1238
|
+
event.kind === 'missing_expected_data.log' ||
|
|
1239
|
+
event.kind === 'missing_required_field.throw' ||
|
|
1240
|
+
event.kind === 'missing_required_field.log')
|
|
1241
|
+
) {
|
|
1242
|
+
event.fieldPath = `${fieldNameOrIndex}.${event.fieldPath}`;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
if (prevErrors != null) {
|
|
1246
|
+
for (let i = this._fieldErrors.length - 1; i >= 0; i--) {
|
|
1247
|
+
prevErrors.push(this._fieldErrors[i]);
|
|
1248
|
+
}
|
|
1249
|
+
this._fieldErrors = prevErrors;
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
this._fieldErrors = prevErrors;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1048
1256
|
_readActorChange(
|
|
1049
1257
|
field: ReaderActorChange,
|
|
1050
1258
|
record: Record,
|
|
@@ -1060,9 +1268,9 @@ class RelayReader {
|
|
|
1060
1268
|
if (externalRef == null) {
|
|
1061
1269
|
data[fieldName] = externalRef;
|
|
1062
1270
|
if (externalRef === undefined) {
|
|
1063
|
-
this._markDataAsMissing();
|
|
1271
|
+
this._markDataAsMissing(fieldName);
|
|
1064
1272
|
} else if (externalRef === null) {
|
|
1065
|
-
this.
|
|
1273
|
+
this._maybeAddFieldErrors(record, storageKey);
|
|
1066
1274
|
}
|
|
1067
1275
|
return data[fieldName];
|
|
1068
1276
|
}
|
|
@@ -1090,8 +1298,13 @@ class RelayReader {
|
|
|
1090
1298
|
): ?mixed {
|
|
1091
1299
|
const storageKey = getStorageKey(field, this._variables);
|
|
1092
1300
|
const linkedIDs = RelayModernRecord.getLinkedRecordIDs(record, storageKey);
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1301
|
+
if (
|
|
1302
|
+
linkedIDs === null ||
|
|
1303
|
+
(RelayFeatureFlags.ENABLE_NONCOMPLIANT_ERROR_HANDLING_ON_LISTS &&
|
|
1304
|
+
Array.isArray(linkedIDs) &&
|
|
1305
|
+
linkedIDs.length === 0)
|
|
1306
|
+
) {
|
|
1307
|
+
this._maybeAddFieldErrors(record, storageKey);
|
|
1095
1308
|
}
|
|
1096
1309
|
return this._readLinkedIds(field, linkedIDs, record, data);
|
|
1097
1310
|
}
|
|
@@ -1107,7 +1320,7 @@ class RelayReader {
|
|
|
1107
1320
|
if (linkedIDs == null) {
|
|
1108
1321
|
data[fieldName] = linkedIDs;
|
|
1109
1322
|
if (linkedIDs === undefined) {
|
|
1110
|
-
this._markDataAsMissing();
|
|
1323
|
+
this._markDataAsMissing(fieldName);
|
|
1111
1324
|
}
|
|
1112
1325
|
return linkedIDs;
|
|
1113
1326
|
}
|
|
@@ -1121,11 +1334,13 @@ class RelayReader {
|
|
|
1121
1334
|
RelayModernRecord.getDataID(record),
|
|
1122
1335
|
prevData,
|
|
1123
1336
|
);
|
|
1337
|
+
const prevErrors = this._fieldErrors;
|
|
1338
|
+
this._fieldErrors = null;
|
|
1124
1339
|
const linkedArray = prevData || [];
|
|
1125
1340
|
linkedIDs.forEach((linkedID, nextIndex) => {
|
|
1126
1341
|
if (linkedID == null) {
|
|
1127
1342
|
if (linkedID === undefined) {
|
|
1128
|
-
this._markDataAsMissing();
|
|
1343
|
+
this._markDataAsMissing(String(nextIndex));
|
|
1129
1344
|
}
|
|
1130
1345
|
// $FlowFixMe[cannot-write]
|
|
1131
1346
|
linkedArray[nextIndex] = linkedID;
|
|
@@ -1140,10 +1355,14 @@ class RelayReader {
|
|
|
1140
1355
|
RelayModernRecord.getDataID(record),
|
|
1141
1356
|
prevItem,
|
|
1142
1357
|
);
|
|
1358
|
+
const prevErrors = this._fieldErrors;
|
|
1359
|
+
this._fieldErrors = null;
|
|
1143
1360
|
// $FlowFixMe[cannot-write]
|
|
1144
1361
|
// $FlowFixMe[incompatible-variance]
|
|
1145
1362
|
linkedArray[nextIndex] = this._traverse(field, linkedID, prevItem);
|
|
1363
|
+
this._prependPreviousErrors(prevErrors, nextIndex);
|
|
1146
1364
|
});
|
|
1365
|
+
this._prependPreviousErrors(prevErrors, fieldName);
|
|
1147
1366
|
data[fieldName] = linkedArray;
|
|
1148
1367
|
return linkedArray;
|
|
1149
1368
|
}
|
|
@@ -1160,13 +1379,18 @@ class RelayReader {
|
|
|
1160
1379
|
// Determine the component module from the store: if the field is missing
|
|
1161
1380
|
// it means we don't know what component to render the match with.
|
|
1162
1381
|
const componentKey = getModuleComponentKey(moduleImport.documentName);
|
|
1382
|
+
const relayStoreComponent = RelayModernRecord.getValue(
|
|
1383
|
+
record,
|
|
1384
|
+
componentKey,
|
|
1385
|
+
);
|
|
1163
1386
|
// componentModuleProvider is used by Client 3D for read time resolvers.
|
|
1164
1387
|
const component =
|
|
1165
|
-
|
|
1166
|
-
|
|
1388
|
+
relayStoreComponent !== undefined
|
|
1389
|
+
? relayStoreComponent
|
|
1390
|
+
: moduleImport.componentModuleProvider;
|
|
1167
1391
|
if (component == null) {
|
|
1168
1392
|
if (component === undefined) {
|
|
1169
|
-
this._markDataAsMissing();
|
|
1393
|
+
this._markDataAsMissing('<module-import>');
|
|
1170
1394
|
}
|
|
1171
1395
|
return;
|
|
1172
1396
|
}
|
|
@@ -1189,6 +1413,39 @@ class RelayReader {
|
|
|
1189
1413
|
data[MODULE_COMPONENT_KEY] = component;
|
|
1190
1414
|
}
|
|
1191
1415
|
|
|
1416
|
+
/**
|
|
1417
|
+
* Aliased inline fragments allow the user to check if the data in an inline
|
|
1418
|
+
* fragment was fetched. Data in the inline fragment can be conditional in the
|
|
1419
|
+
* case of a type condition on the inline fragment or directives like `@skip`
|
|
1420
|
+
* or `@include`.
|
|
1421
|
+
*
|
|
1422
|
+
* We model aliased inline fragments as a special reader node wrapped around a
|
|
1423
|
+
* regular inline fragment reader node.
|
|
1424
|
+
*
|
|
1425
|
+
* This allows us to read the inline fragment as normal, check if it matched,
|
|
1426
|
+
* and then define the alias to either contain the inline fragment's data, or
|
|
1427
|
+
* null.
|
|
1428
|
+
*/
|
|
1429
|
+
_readAliasedInlineFragment(
|
|
1430
|
+
aliasedInlineFragment: ReaderAliasedInlineFragmentSpread,
|
|
1431
|
+
record: Record,
|
|
1432
|
+
data: SelectorData,
|
|
1433
|
+
) {
|
|
1434
|
+
const prevErrors = this._fieldErrors;
|
|
1435
|
+
this._fieldErrors = null;
|
|
1436
|
+
let fieldValue = this._readInlineFragment(
|
|
1437
|
+
aliasedInlineFragment.fragment,
|
|
1438
|
+
record,
|
|
1439
|
+
{},
|
|
1440
|
+
true,
|
|
1441
|
+
);
|
|
1442
|
+
this._prependPreviousErrors(prevErrors, aliasedInlineFragment.name);
|
|
1443
|
+
if (fieldValue === false) {
|
|
1444
|
+
fieldValue = null;
|
|
1445
|
+
}
|
|
1446
|
+
data[aliasedInlineFragment.name] = fieldValue;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1192
1449
|
// Has three possible return values:
|
|
1193
1450
|
// * null: The type condition did not match
|
|
1194
1451
|
// * undefined: We are missing data
|
|
@@ -1398,16 +1655,14 @@ class RelayReader {
|
|
|
1398
1655
|
// fetched the `__is[AbstractType]` flag for this concrete type. In this
|
|
1399
1656
|
// case we need to report that we are missing data, in case that field is
|
|
1400
1657
|
// still in flight.
|
|
1401
|
-
this._markDataAsMissing();
|
|
1658
|
+
this._markDataAsMissing('<abstract-type-hint>');
|
|
1402
1659
|
}
|
|
1403
1660
|
// $FlowFixMe Casting record value
|
|
1404
1661
|
return implementsInterface;
|
|
1405
1662
|
}
|
|
1406
1663
|
}
|
|
1407
1664
|
|
|
1408
|
-
function markFieldErrorHasHandled(
|
|
1409
|
-
event: ErrorResponseField,
|
|
1410
|
-
): ErrorResponseField {
|
|
1665
|
+
function markFieldErrorHasHandled(event: FieldError): FieldError {
|
|
1411
1666
|
switch (event.kind) {
|
|
1412
1667
|
case 'missing_expected_data.throw':
|
|
1413
1668
|
case 'missing_required_field.throw':
|