diff --git a/packages/react-relay/__tests__/ReactRelayFragmentContainer-WithFragmentOwnership-test.js b/packages/react-relay/__tests__/ReactRelayFragmentContainer-WithFragmentOwnership-test.js index 2140bb5f1338..cb30b57d7e10 100644 --- a/packages/react-relay/__tests__/ReactRelayFragmentContainer-WithFragmentOwnership-test.js +++ b/packages/react-relay/__tests__/ReactRelayFragmentContainer-WithFragmentOwnership-test.js @@ -206,7 +206,6 @@ describe('ReactRelayFragmentContainer with fragment ownership', () => { }, __fragmentOwner: ownerUser1.request, }, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -334,7 +333,6 @@ describe('ReactRelayFragmentContainer with fragment ownership', () => { }, __fragmentOwner: ownerUser2.request, }, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -405,7 +403,6 @@ describe('ReactRelayFragmentContainer with fragment ownership', () => { }, __fragmentOwner: ownerUser1WithCondVar.request, }, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, diff --git a/packages/react-relay/__tests__/ReactRelayFragmentContainer-test.js b/packages/react-relay/__tests__/ReactRelayFragmentContainer-test.js index 91f8bb325954..40d9f3d73c6c 100644 --- a/packages/react-relay/__tests__/ReactRelayFragmentContainer-test.js +++ b/packages/react-relay/__tests__/ReactRelayFragmentContainer-test.js @@ -270,7 +270,6 @@ describe('ReactRelayFragmentContainer', () => { id: '4', name: 'Zuck', }, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -372,7 +371,6 @@ describe('ReactRelayFragmentContainer', () => { name: 'Joe', }, isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -431,7 +429,6 @@ describe('ReactRelayFragmentContainer', () => { // Name is excluded since value of cond is now false }, isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, diff --git a/packages/react-relay/__tests__/ReactRelayPaginationContainer-test.js b/packages/react-relay/__tests__/ReactRelayPaginationContainer-test.js index 034168507474..19f60ef692c7 100644 --- a/packages/react-relay/__tests__/ReactRelayPaginationContainer-test.js +++ b/packages/react-relay/__tests__/ReactRelayPaginationContainer-test.js @@ -343,7 +343,6 @@ describe('ReactRelayPaginationContainer', () => { expect(environment.subscribe.mock.calls[0][0]).toEqual({ data: expect.any(Object), isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -457,7 +456,6 @@ describe('ReactRelayPaginationContainer', () => { expect(environment.subscribe.mock.calls[0][0]).toEqual({ data: expect.any(Object), isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -527,7 +525,6 @@ describe('ReactRelayPaginationContainer', () => { expect(environment.subscribe.mock.calls[0][0]).toEqual({ data: expect.any(Object), isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -632,7 +629,6 @@ describe('ReactRelayPaginationContainer', () => { expect(environment.subscribe.mock.calls[0][0]).toEqual({ data: expect.any(Object), isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, diff --git a/packages/react-relay/__tests__/ReactRelayRefetchContainer-test.js b/packages/react-relay/__tests__/ReactRelayRefetchContainer-test.js index 8e3d5490a507..56b2ceb8a3a1 100644 --- a/packages/react-relay/__tests__/ReactRelayRefetchContainer-test.js +++ b/packages/react-relay/__tests__/ReactRelayRefetchContainer-test.js @@ -279,7 +279,6 @@ describe('ReactRelayRefetchContainer', () => { name: 'Zuck', }, isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -380,7 +379,6 @@ describe('ReactRelayRefetchContainer', () => { name: 'Joe', }, isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -440,7 +438,6 @@ describe('ReactRelayRefetchContainer', () => { // Name is excluded since value of cond is now false }, isMissingData: false, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -527,7 +524,6 @@ describe('ReactRelayRefetchContainer', () => { id: '4', // Name is excluded since value of cond is now false }, - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, diff --git a/packages/react-relay/relay-hooks/__tests__/useFragment_nullability-test.js b/packages/react-relay/relay-hooks/__tests__/useFragment_nullability-test.js index f7968e40bac2..0a639bb999a4 100644 --- a/packages/react-relay/relay-hooks/__tests__/useFragment_nullability-test.js +++ b/packages/react-relay/relay-hooks/__tests__/useFragment_nullability-test.js @@ -82,7 +82,9 @@ describe('useFragment_nullability-test.js', () => { ); await TestRenderer.act(() => jest.runAllTimers()); expect( - String(renderer.toJSON()).includes('Unexpected response payload'), + String(renderer.toJSON()).includes( + "Resolver error at path 'field_that_throws' in 'useFragmentNullabilityTest1Query'.", + ), ).toEqual(true); }); @@ -110,7 +112,9 @@ describe('useFragment_nullability-test.js', () => { ); await TestRenderer.act(() => jest.runAllTimers()); expect( - String(renderer.toJSON()).includes('Unexpected response payload'), + String(renderer.toJSON()).includes( + "Resolver error at path 'field_that_throws' in 'useFragmentNullabilityTestFragmentWithFieldThatThrows'.", + ), ).toEqual(true); }); diff --git a/packages/react-relay/relay-hooks/legacy/FragmentResource.js b/packages/react-relay/relay-hooks/legacy/FragmentResource.js index e09afd90f6a4..f12ab7f76360 100644 --- a/packages/react-relay/relay-hooks/legacy/FragmentResource.js +++ b/packages/react-relay/relay-hooks/legacy/FragmentResource.js @@ -559,19 +559,12 @@ class FragmentResourceImpl { _throwOrLogErrorsInSnapshot(snapshot: SingularOrPluralSnapshot) { if (Array.isArray(snapshot)) { snapshot.forEach(s => { - handlePotentialSnapshotErrors( - this._environment, - s.missingRequiredFields, - s.errorResponseFields, - s.selector.node.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(this._environment, s.errorResponseFields); }); } else { handlePotentialSnapshotErrors( this._environment, - snapshot.missingRequiredFields, snapshot.errorResponseFields, - snapshot.selector.node.metadata?.throwOnFieldError ?? false, ); } } @@ -771,7 +764,6 @@ class FragmentResourceImpl { missingLiveResolverFields: currentSnapshot.missingLiveResolverFields, seenRecords: currentSnapshot.seenRecords, selector: currentSnapshot.selector, - missingRequiredFields: currentSnapshot.missingRequiredFields, errorResponseFields: currentSnapshot.errorResponseFields, }; if (updatedData !== renderData) { diff --git a/packages/react-relay/relay-hooks/readFragmentInternal.js b/packages/react-relay/relay-hooks/readFragmentInternal.js index 35d92ff3cead..4bde1922432a 100644 --- a/packages/react-relay/relay-hooks/readFragmentInternal.js +++ b/packages/react-relay/relay-hooks/readFragmentInternal.js @@ -85,18 +85,11 @@ function handlePotentialSnapshotErrorsForState( if (state.kind === 'singular') { handlePotentialSnapshotErrors( environment, - state.snapshot.missingRequiredFields, state.snapshot.errorResponseFields, - state.snapshot.selector.node.metadata?.throwOnFieldError ?? false, ); } else if (state.kind === 'plural') { for (const snapshot of state.snapshots) { - handlePotentialSnapshotErrors( - environment, - snapshot.missingRequiredFields, - snapshot.errorResponseFields, - snapshot.selector.node.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields); } } } diff --git a/packages/react-relay/relay-hooks/useFragmentInternal_CURRENT.js b/packages/react-relay/relay-hooks/useFragmentInternal_CURRENT.js index 937d64963647..7ebfca66e4c6 100644 --- a/packages/react-relay/relay-hooks/useFragmentInternal_CURRENT.js +++ b/packages/react-relay/relay-hooks/useFragmentInternal_CURRENT.js @@ -115,18 +115,11 @@ function handlePotentialSnapshotErrorsForState( if (state.kind === 'singular') { handlePotentialSnapshotErrors( environment, - state.snapshot.missingRequiredFields, state.snapshot.errorResponseFields, - state.snapshot.selector.node.metadata?.throwOnFieldError ?? false, ); } else if (state.kind === 'plural') { for (const snapshot of state.snapshots) { - handlePotentialSnapshotErrors( - environment, - snapshot.missingRequiredFields, - snapshot.errorResponseFields, - snapshot.selector.node.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields); } } } @@ -162,7 +155,6 @@ function handleMissedUpdates( missingLiveResolverFields: currentSnapshot.missingLiveResolverFields, seenRecords: currentSnapshot.seenRecords, selector: currentSnapshot.selector, - missingRequiredFields: currentSnapshot.missingRequiredFields, errorResponseFields: currentSnapshot.errorResponseFields, }; return [ @@ -187,7 +179,6 @@ function handleMissedUpdates( missingLiveResolverFields: currentSnapshot.missingLiveResolverFields, seenRecords: currentSnapshot.seenRecords, selector: currentSnapshot.selector, - missingRequiredFields: currentSnapshot.missingRequiredFields, errorResponseFields: currentSnapshot.errorResponseFields, }; if (updatedData !== snapshot.data) { diff --git a/packages/react-relay/relay-hooks/useFragmentInternal_EXPERIMENTAL.js b/packages/react-relay/relay-hooks/useFragmentInternal_EXPERIMENTAL.js index 72f95dbfc50d..db7e135b9698 100644 --- a/packages/react-relay/relay-hooks/useFragmentInternal_EXPERIMENTAL.js +++ b/packages/react-relay/relay-hooks/useFragmentInternal_EXPERIMENTAL.js @@ -127,18 +127,11 @@ function handlePotentialSnapshotErrorsForState( if (state.kind === 'singular') { handlePotentialSnapshotErrors( environment, - state.snapshot.missingRequiredFields, state.snapshot.errorResponseFields, - state.snapshot.selector.node.metadata?.throwOnFieldError ?? false, ); } else if (state.kind === 'plural') { for (const snapshot of state.snapshots) { - handlePotentialSnapshotErrors( - environment, - snapshot.missingRequiredFields, - snapshot.errorResponseFields, - snapshot.selector.node.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields); } } } @@ -174,7 +167,6 @@ function handleMissedUpdates( missingLiveResolverFields: currentSnapshot.missingLiveResolverFields, seenRecords: currentSnapshot.seenRecords, selector: currentSnapshot.selector, - missingRequiredFields: currentSnapshot.missingRequiredFields, errorResponseFields: currentSnapshot.errorResponseFields, }; return [ @@ -201,7 +193,6 @@ function handleMissedUpdates( missingLiveResolverFields: currentSnapshot.missingLiveResolverFields, seenRecords: currentSnapshot.seenRecords, selector: currentSnapshot.selector, - missingRequiredFields: currentSnapshot.missingRequiredFields, errorResponseFields: currentSnapshot.errorResponseFields, }; if (updatedData !== snapshot.data) { diff --git a/packages/relay-runtime/index.js b/packages/relay-runtime/index.js index e90b59e76d2c..f9e662bde99c 100644 --- a/packages/relay-runtime/index.js +++ b/packages/relay-runtime/index.js @@ -134,7 +134,6 @@ export type { LogEvent, LogFunction, MissingFieldHandler, - MissingRequiredFields, ModuleImportPointer, MutableRecordSource, MutationParameters, diff --git a/packages/relay-runtime/query/fetchQuery.js b/packages/relay-runtime/query/fetchQuery.js index 9ab5dc7a8589..f71225079e26 100644 --- a/packages/relay-runtime/query/fetchQuery.js +++ b/packages/relay-runtime/query/fetchQuery.js @@ -136,12 +136,7 @@ function fetchQuery( const fetchPolicy = options?.fetchPolicy ?? 'network-only'; function readData(snapshot: Snapshot): TData { - handlePotentialSnapshotErrors( - environment, - snapshot.missingRequiredFields, - snapshot.errorResponseFields, - queryNode.fragment.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields); /* $FlowFixMe[incompatible-return] we assume readData returns the right * data just having written it from network or checked availability. */ return snapshot.data; diff --git a/packages/relay-runtime/store/RelayErrorTrie.js b/packages/relay-runtime/store/RelayErrorTrie.js index 5ee8a4aeac10..b466e8654e07 100644 --- a/packages/relay-runtime/store/RelayErrorTrie.js +++ b/packages/relay-runtime/store/RelayErrorTrie.js @@ -16,16 +16,6 @@ import type {PayloadError} from '../network/RelayNetworkTypes'; // $FlowFixMe[recursive-definition] const SELF: Self = Symbol('$SELF'); -class RelayFieldError extends Error { - constructor(message: string, errors: Array = []) { - super(message); - this.name = 'RelayFieldError'; - this.message = message; - this.errors = errors; - } - errors: Array; -} - export opaque type Self = typeof SELF; export type TRelayFieldErrorForDisplay = $ReadOnly<{ @@ -180,11 +170,9 @@ module.exports = ({ buildErrorTrie, getNestedErrorTrieByKey, getErrorsByKey, - RelayFieldError, }: { SELF: typeof SELF, buildErrorTrie: typeof buildErrorTrie, getNestedErrorTrieByKey: typeof getNestedErrorTrieByKey, getErrorsByKey: typeof getErrorsByKey, - RelayFieldError: Class, }); diff --git a/packages/relay-runtime/store/RelayModernFragmentSpecResolver.js b/packages/relay-runtime/store/RelayModernFragmentSpecResolver.js index af2cb722886d..bb9c948fa202 100644 --- a/packages/relay-runtime/store/RelayModernFragmentSpecResolver.js +++ b/packages/relay-runtime/store/RelayModernFragmentSpecResolver.js @@ -19,7 +19,6 @@ import type { FragmentSpecResolver, FragmentSpecResults, IEnvironment, - MissingRequiredFields, PluralReaderSelector, RelayContext, SelectorData, @@ -227,7 +226,6 @@ class SelectorResolver { _data: ?SelectorData; _environment: IEnvironment; _isMissingData: boolean; - _missingRequiredFields: ?MissingRequiredFields; _errorResponseFields: ?ErrorResponseFields; _rootIsQueryRenderer: boolean; _selector: SingularReaderSelector; @@ -244,7 +242,6 @@ class SelectorResolver { this._callback = callback; this._data = snapshot.data; this._isMissingData = snapshot.isMissingData; - this._missingRequiredFields = snapshot.missingRequiredFields; this._errorResponseFields = snapshot.errorResponseFields; this._environment = environment; this._rootIsQueryRenderer = rootIsQueryRenderer; @@ -326,12 +323,7 @@ class SelectorResolver { } } } - handlePotentialSnapshotErrors( - this._environment, - this._missingRequiredFields, - this._errorResponseFields, - this._selector.node.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(this._environment, this._errorResponseFields); return this._data; } @@ -346,7 +338,6 @@ class SelectorResolver { const snapshot = this._environment.lookup(selector); this._data = recycleNodesInto(this._data, snapshot.data); this._isMissingData = snapshot.isMissingData; - this._missingRequiredFields = snapshot.missingRequiredFields; this._errorResponseFields = snapshot.errorResponseFields; this._selector = selector; this._subscription = this._environment.subscribe(snapshot, this._onChange); @@ -383,7 +374,6 @@ class SelectorResolver { _onChange = (snapshot: Snapshot): void => { this._data = snapshot.data; this._isMissingData = snapshot.isMissingData; - this._missingRequiredFields = snapshot.missingRequiredFields; this._errorResponseFields = snapshot.errorResponseFields; this._callback(); }; diff --git a/packages/relay-runtime/store/RelayReader.js b/packages/relay-runtime/store/RelayReader.js index 336f2c942be2..3022454baafa 100644 --- a/packages/relay-runtime/store/RelayReader.js +++ b/packages/relay-runtime/store/RelayReader.js @@ -35,7 +35,6 @@ import type { ErrorResponseFields, MissingClientEdgeRequestInfo, MissingLiveResolverField, - MissingRequiredFields, Record, RecordSource, RequestDescriptor, @@ -99,7 +98,6 @@ class RelayReader { _missingClientEdges: Array; _missingLiveResolverFields: Array; _isWithinUnmatchedTypeRefinement: boolean; - _missingRequiredFields: ?MissingRequiredFields; _errorResponseFields: ?ErrorResponseFields; _owner: RequestDescriptor; _recordSource: RecordSource; @@ -124,7 +122,6 @@ class RelayReader { this._missingLiveResolverFields = []; this._isMissingData = false; this._isWithinUnmatchedTypeRefinement = false; - this._missingRequiredFields = null; this._errorResponseFields = null; this._owner = selector.owner; this._recordSource = recordSource; @@ -193,7 +190,6 @@ class RelayReader { missingLiveResolverFields: this._missingLiveResolverFields, seenRecords: this._seenRecords, selector: this._selector, - missingRequiredFields: this._missingRequiredFields, errorResponseFields: this._errorResponseFields, }; } @@ -284,33 +280,26 @@ class RelayReader { } _maybeReportUnexpectedNull(fieldPath: string, action: 'LOG' | 'THROW') { - if (this._missingRequiredFields?.action === 'THROW') { - // Chained @required directives may cause a parent `@required(action: - // THROW)` field to become null, so the first missing field we - // encounter is likely to be the root cause of the error. - return; - } const owner = this._fragmentName; + if (this._errorResponseFields == null) { + this._errorResponseFields = []; + } + switch (action) { case 'THROW': - this._missingRequiredFields = {action, field: {path: fieldPath, owner}}; + this._errorResponseFields.push({ + kind: 'missing_required_field.throw', + fieldPath, + owner, + }); return; case 'LOG': - if (this._missingRequiredFields == null) { - this._missingRequiredFields = { - action, - fields: [{path: fieldPath, owner}], - }; - } else { - this._missingRequiredFields = { - action, - fields: [ - ...this._missingRequiredFields.fields, - {path: fieldPath, owner}, - ], - }; - } + this._errorResponseFields.push({ + kind: 'missing_required_field.log', + fieldPath, + owner, + }); return; default: (action: empty); @@ -333,43 +322,40 @@ class RelayReader { "Couldn't determine field name for this field. It might be a ReaderClientExtension - which is not yet supported.", ); - let errors = this._errorResponseFields?.map(error => { - switch (error.kind) { - case 'relay_field_payload.error': - const {message, ...displayError} = error.error; - return displayError; - case 'missing_expected_data.throw': - case 'missing_expected_data.log': - return { - path: error.fieldPath.split('.'), - }; - case 'relay_resolver.error': - return { - message: `Relay: Error in resolver for field at ${error.fieldPath} in ${error.owner}`, - }; - default: - (error.kind: empty); - invariant( - false, - 'Unexpected error errorResponseField kind: %s', - error.kind, - ); - } - }); - - // If we have a nested @required(THROW) that will throw, - // we want to catch that error and provide it - if (this._missingRequiredFields?.action === 'THROW') { - const {owner, path} = this._missingRequiredFields.field; - const missingFieldError = { - message: `Relay: Missing @required value at path '${path}' in '${owner}'.`, - }; - if (errors == null) { - errors = [missingFieldError]; - } else { - errors.push(missingFieldError); - } - } + const errors = this._errorResponseFields + ?.map(error => { + switch (error.kind) { + case 'relay_field_payload.error': + const {message, ...displayError} = error.error; + return displayError; + case 'missing_expected_data.throw': + case 'missing_expected_data.log': + return { + path: error.fieldPath.split('.'), + }; + case 'relay_resolver.error': + return { + message: `Relay: Error in resolver for field at ${error.fieldPath} in ${error.owner}`, + }; + case 'missing_required_field.throw': + // If we have a nested @required(THROW) that will throw, + // we want to catch that error and provide it + return { + message: `Relay: Missing @required value at path '${error.fieldPath}' in '${error.owner}'.`, + }; + case 'missing_required_field.log': + // For backwards compatibility, we don't surface log level missing required fields + return null; + default: + (error.kind: empty); + invariant( + false, + 'Unexpected error errorResponseField kind: %s', + error.kind, + ); + } + }) + .filter(Boolean); data[fieldName] = errors != null ? {ok: false, errors} : {ok: true, value}; } @@ -412,10 +398,8 @@ class RelayReader { break; case 'CatchField': { const previousResponseFields = this._errorResponseFields; - const previousMissingRequiredFields = this._missingRequiredFields; this._errorResponseFields = null; - this._missingRequiredFields = null; const catchFieldValue = this._readClientSideDirectiveField( selection, @@ -427,15 +411,23 @@ class RelayReader { this._handleCatchToResult(selection, record, data, catchFieldValue); } - const childrenMissingRequiredFields = this._missingRequiredFields; + const childrenErrorResponseFields = this._errorResponseFields; this._errorResponseFields = previousResponseFields; - this._missingRequiredFields = previousMissingRequiredFields; // If we encountered non-throwing @required fields, in the children, // we want to preserve those errors in the snapshot. - if (childrenMissingRequiredFields?.action === 'LOG') { - this._addMissingRequiredFields(childrenMissingRequiredFields); + if (childrenErrorResponseFields != null) { + for (let i = 0; i < childrenErrorResponseFields.length; i++) { + const event = childrenErrorResponseFields[i]; + if (event.kind === 'missing_required_field.log') { + if (this._errorResponseFields == null) { + this._errorResponseFields = [event]; + } else { + this._errorResponseFields.push(event); + } + } + } } break; @@ -647,7 +639,7 @@ class RelayReader { return { data: snapshot.data, isMissingData: snapshot.isMissingData, - missingRequiredFields: snapshot.missingRequiredFields, + errorResponseFields: snapshot.errorResponseFields, }; } @@ -660,7 +652,7 @@ class RelayReader { return { data: snapshot.data, isMissingData: snapshot.isMissingData, - missingRequiredFields: snapshot.missingRequiredFields, + errorResponseFields: snapshot.errorResponseFields, }; }; @@ -744,9 +736,6 @@ class RelayReader { // errors, or be in a suspended state. Here we propagate those cases // upwards to mimic the behavior of having traversed into that fragment directly. if (cachedSnapshot != null) { - if (cachedSnapshot.missingRequiredFields != null) { - this._addMissingRequiredFields(cachedSnapshot.missingRequiredFields); - } if (cachedSnapshot.missingClientEdges != null) { for (const missing of cachedSnapshot.missingClientEdges) { this._missingClientEdges.push(missing); @@ -767,9 +756,13 @@ class RelayReader { } for (const error of cachedSnapshot.errorResponseFields) { // TODO: In reality we should propagate _all_ errors, but - // for now we're only propagating resolver errors for backwards - // compatibility with previous behavior. - if (error.kind === 'relay_resolver.error') { + // for now we're only propagating resolver errors and missing field + // errors for backwards compatibility with previous behavior. + if ( + error.kind === 'relay_resolver.error' || + error.kind === 'missing_required_field.throw' || + error.kind === 'missing_required_field.log' + ) { this._errorResponseFields.push(error); } } @@ -1392,26 +1385,6 @@ class RelayReader { fragmentPointers[fragmentSpreadOrFragment.name] = inlineData; } - _addMissingRequiredFields(additional: MissingRequiredFields) { - if (this._missingRequiredFields == null) { - this._missingRequiredFields = additional; - return; - } - - if (this._missingRequiredFields.action === 'THROW') { - return; - } - if (additional.action === 'THROW') { - this._missingRequiredFields = additional; - return; - } - - this._missingRequiredFields = { - action: 'LOG', - fields: [...this._missingRequiredFields.fields, ...additional.fields], - }; - } - _implementsInterface(record: Record, abstractKey: string): ?boolean { const typeName = RelayModernRecord.getType(record); const typeRecord = this._recordSource.get(generateTypeID(typeName)); diff --git a/packages/relay-runtime/store/RelayStoreSubscriptions.js b/packages/relay-runtime/store/RelayStoreSubscriptions.js index a3f30fa82c11..bfdd489656bb 100644 --- a/packages/relay-runtime/store/RelayStoreSubscriptions.js +++ b/packages/relay-runtime/store/RelayStoreSubscriptions.js @@ -115,7 +115,6 @@ class RelayStoreSubscriptions implements StoreSubscriptions { missingLiveResolverFields: backup.missingLiveResolverFields, seenRecords: backup.seenRecords, selector: backup.selector, - missingRequiredFields: backup.missingRequiredFields, errorResponseFields: backup.errorResponseFields, }; } else { @@ -186,7 +185,6 @@ class RelayStoreSubscriptions implements StoreSubscriptions { missingLiveResolverFields: nextSnapshot.missingLiveResolverFields, seenRecords: nextSnapshot.seenRecords, selector: nextSnapshot.selector, - missingRequiredFields: nextSnapshot.missingRequiredFields, errorResponseFields: nextSnapshot.errorResponseFields, }: Snapshot); if (__DEV__) { diff --git a/packages/relay-runtime/store/RelayStoreTypes.js b/packages/relay-runtime/store/RelayStoreTypes.js index 0a21320064ad..b3570493bdb6 100644 --- a/packages/relay-runtime/store/RelayStoreTypes.js +++ b/packages/relay-runtime/store/RelayStoreTypes.js @@ -116,22 +116,15 @@ export type NormalizationSelector = { +variables: Variables, }; -type FieldLocation = { - path: string, - owner: string, -}; - -export type MissingRequiredFields = $ReadOnly< - | {action: 'THROW', field: FieldLocation} - | {action: 'LOG', fields: Array}, ->; - -export type ErrorResponseFields = Array< +export type ErrorResponseField = | RelayFieldPayloadErrorEvent | MissingExpectedDataLogEvent | MissingExpectedDataThrowEvent - | RelayResolverErrorEvent, ->; + | RelayResolverErrorEvent + | MissingRequiredFieldLogEvent + | MissingRequiredFieldThrowEvent; + +export type ErrorResponseFields = Array; export type ClientEdgeTraversalInfo = { +readerClientEdge: ReaderClientEdgeToServerObject, @@ -161,7 +154,6 @@ export type Snapshot = { +missingClientEdges: null | $ReadOnlyArray, +seenRecords: DataIDSet, +selector: SingularReaderSelector, - +missingRequiredFields: ?MissingRequiredFields, +errorResponseFields: ?ErrorResponseFields, }; @@ -1303,7 +1295,7 @@ export type MissingExpectedDataThrowEvent = { * A field was marked as @required(action: LOG) but was null or missing in the * store. */ -export type MissingFieldLogEvent = { +export type MissingRequiredFieldLogEvent = { +kind: 'missing_required_field.log', +owner: string, +fieldPath: string, @@ -1316,7 +1308,7 @@ export type MissingFieldLogEvent = { * Relay will throw immediately after logging this event. If you wish to * customize the error being thrown, you may throw your own error. */ -export type MissingFieldThrowEvent = { +export type MissingRequiredFieldThrowEvent = { +kind: 'missing_required_field.throw', +owner: string, +fieldPath: string, @@ -1365,8 +1357,8 @@ export type RelayFieldPayloadErrorEvent = { export type RelayFieldLoggerEvent = | MissingExpectedDataLogEvent | MissingExpectedDataThrowEvent - | MissingFieldLogEvent - | MissingFieldThrowEvent + | MissingRequiredFieldLogEvent + | MissingRequiredFieldThrowEvent | RelayResolverErrorEvent | RelayFieldPayloadErrorEvent; diff --git a/packages/relay-runtime/store/ResolverCache.js b/packages/relay-runtime/store/ResolverCache.js index 4bab1d35e86d..301d5fc1c663 100644 --- a/packages/relay-runtime/store/ResolverCache.js +++ b/packages/relay-runtime/store/ResolverCache.js @@ -11,7 +11,6 @@ 'use strict'; -import type {MissingRequiredFields} from '..'; import type { ReaderRelayLiveResolver, ReaderRelayResolver, @@ -19,6 +18,7 @@ import type { import type {DataID, Variables} from '../util/RelayRuntimeTypes'; import type { DataIDSet, + ErrorResponseFields, MutableRecordSource, Record, SingularReaderSelector, @@ -52,7 +52,7 @@ export type EvaluationResult = { export type ResolverFragmentResult = { data: mixed, isMissingData: boolean, - missingRequiredFields: ?MissingRequiredFields, + errorResponseFields: ?ErrorResponseFields, }; export type GetDataForResolverFragmentFn = diff --git a/packages/relay-runtime/store/ResolverFragments.js b/packages/relay-runtime/store/ResolverFragments.js index 90e39324c787..32dcc7d2309a 100644 --- a/packages/relay-runtime/store/ResolverFragments.js +++ b/packages/relay-runtime/store/ResolverFragments.js @@ -111,13 +111,16 @@ function readFragment( fragmentSelector.kind === 'SingularReaderSelector', `Expected a singular reader selector for the fragment of the resolver ${fragmentNode.name}, but it was plural.`, ); - const {data, isMissingData, missingRequiredFields} = + const {data, isMissingData, errorResponseFields} = context.getDataForResolverFragment(fragmentSelector, fragmentKey); if ( isMissingData || - (missingRequiredFields != null && missingRequiredFields.action === 'THROW') - // TODO: Also consider @throwOnFieldError + (errorResponseFields != null && + errorResponseFields.some( + // TODO: Also consider @throwOnFieldError + event => event.kind === 'missing_required_field.throw', + )) ) { throw RESOLVER_FRAGMENT_ERRORED_SENTINEL; } diff --git a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ConnectionAndRequired-test.js b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ConnectionAndRequired-test.js index 9db002ad7dfc..d1a8a0664ed6 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ConnectionAndRequired-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ConnectionAndRequired-test.js @@ -130,16 +130,14 @@ describe.each(['RelayModernEnvironment', 'MultiActorEnvironment'])( getSingularSelector(fragment, nextOperationSnapshot.data?.node), ); const snapshot = environment.lookup(selector); - expect(snapshot.missingRequiredFields).toEqual({ - action: 'LOG', - fields: [ - { - owner: - 'RelayModernEnvironmentConnectionAndRequiredTestFeedbackFragment', - path: 'comments', - }, - ], - }); + expect(snapshot.errorResponseFields).toEqual([ + { + kind: 'missing_required_field.log', + owner: + 'RelayModernEnvironmentConnectionAndRequiredTestFeedbackFragment', + fieldPath: 'comments', + }, + ]); expect(snapshot.data).toEqual(null); }); }); diff --git a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteWithStreamAndRequired-test.js b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteWithStreamAndRequired-test.js index 5349e2eee879..52b81726d03d 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteWithStreamAndRequired-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernEnvironment-ExecuteWithStreamAndRequired-test.js @@ -111,16 +111,14 @@ describe('execute() a query with @stream and @required', () => { jest.runAllTimers(); const snapshot = callback.mock.calls[0][0]; - expect(snapshot.missingRequiredFields).toEqual({ - action: 'LOG', - fields: [ - { - owner: - 'RelayModernEnvironmentExecuteWithStreamAndRequiredTestFeedbackFragment', - path: 'actors', - }, - ], - }); + expect(snapshot.errorResponseFields).toEqual([ + { + kind: 'missing_required_field.log', + owner: + 'RelayModernEnvironmentExecuteWithStreamAndRequiredTestFeedbackFragment', + fieldPath: 'actors', + }, + ]); expect(snapshot.data).toEqual(null); }); }); diff --git a/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js b/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js index 55820e842007..6dc0a6eaa0de 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernStore-Subscriptions-test.js @@ -369,7 +369,6 @@ function cloneEventWithSets(event: LogEvent) { expect(callback.mock.calls.length).toBe(1); expect(callback.mock.calls[0][0]).toEqual({ ...snapshot, - missingRequiredFields: null, missingLiveResolverFields: [], isMissingData: false, errorResponseFields: null, @@ -414,7 +413,6 @@ function cloneEventWithSets(event: LogEvent) { name: 'Joe', profilePicture: undefined, }, - missingRequiredFields: null, missingLiveResolverFields: [], isMissingData: true, errorResponseFields: [ @@ -466,7 +464,6 @@ function cloneEventWithSets(event: LogEvent) { name: 'Joe', profilePicture: undefined, }, - missingRequiredFields: null, errorResponseFields: [ { fieldPath: '', diff --git a/packages/relay-runtime/store/__tests__/RelayModernStore-test.js b/packages/relay-runtime/store/__tests__/RelayModernStore-test.js index ad4955fd6bce..b7f25056702e 100644 --- a/packages/relay-runtime/store/__tests__/RelayModernStore-test.js +++ b/packages/relay-runtime/store/__tests__/RelayModernStore-test.js @@ -285,7 +285,6 @@ function cloneEventWithSets(event: LogEvent) { }, }, seenRecords: new Set(Object.keys(data)), - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -339,7 +338,6 @@ function cloneEventWithSets(event: LogEvent) { __fragmentOwner: owner.request, }, seenRecords: new Set(Object.keys(data)), - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -397,7 +395,6 @@ function cloneEventWithSets(event: LogEvent) { }, }, seenRecords: new Set(['client:2', '4']), - missingRequiredFields: null, errorResponseFields: null, missingLiveResolverFields: [], missingClientEdges: null, @@ -680,7 +677,6 @@ function cloneEventWithSets(event: LogEvent) { expect(callback.mock.calls.length).toBe(1); expect(callback.mock.calls[0][0]).toEqual({ ...snapshot, - missingRequiredFields: null, missingClientEdges: null, isMissingData: false, errorResponseFields: null, @@ -737,7 +733,6 @@ function cloneEventWithSets(event: LogEvent) { fieldPath: '', }, ], - missingRequiredFields: null, missingLiveResolverFields: [], missingClientEdges: null, isMissingData: true, @@ -778,7 +773,6 @@ function cloneEventWithSets(event: LogEvent) { name: 'Joe', profilePicture: undefined, }, - missingRequiredFields: null, missingClientEdges: null, isMissingData: true, errorResponseFields: [ diff --git a/packages/relay-runtime/store/__tests__/RelayReader-AliasedFragments-test.js b/packages/relay-runtime/store/__tests__/RelayReader-AliasedFragments-test.js index 7d138754e516..9d55ce9b320e 100644 --- a/packages/relay-runtime/store/__tests__/RelayReader-AliasedFragments-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReader-AliasedFragments-test.js @@ -906,8 +906,8 @@ describe('Inline Fragments', () => { `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); const snapshot = read(source, operation.fragment); - const {data, isMissingData, missingRequiredFields} = snapshot; - expect(missingRequiredFields).toBe(null); + const {data, isMissingData, errorResponseFields} = snapshot; + expect(errorResponseFields).toBe(null); expect(isMissingData).toBe(false); expect(data).toEqual({ node: { @@ -1035,20 +1035,24 @@ describe('Inline Fragments', () => { } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, isMissingData, missingRequiredFields} = read( + const {data, isMissingData, errorResponseFields} = read( source, operation.fragment, ); - expect(missingRequiredFields).toEqual({ - action: 'LOG', - fields: [ - { - owner: - 'RelayReaderAliasedFragmentsTestRequiredBubblesOnAbstractTypeQuery', - path: 'node.aliased_fragment.name', - }, - ], - }); + expect(errorResponseFields).toEqual([ + { + fieldPath: '', + kind: 'missing_expected_data.log', + owner: + 'RelayReaderAliasedFragmentsTestRequiredBubblesOnAbstractTypeQuery', + }, + { + fieldPath: 'node.aliased_fragment.name', + kind: 'missing_required_field.log', + owner: + 'RelayReaderAliasedFragmentsTestRequiredBubblesOnAbstractTypeQuery', + }, + ]); expect(isMissingData).toBe(true); expect(data).toEqual({ node: { @@ -1091,20 +1095,24 @@ describe('Inline Fragments', () => { } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, isMissingData, missingRequiredFields} = read( + const {data, isMissingData, errorResponseFields} = read( source, operation.fragment, ); - expect(missingRequiredFields).toEqual({ - action: 'LOG', - fields: [ - { - owner: - 'RelayReaderAliasedFragmentsTestRequiredBubblesOnAbstractWithMissingTypeInfoQuery', - path: 'node.aliased_fragment.name', - }, - ], - }); + expect(errorResponseFields).toEqual([ + { + fieldPath: '', + kind: 'missing_expected_data.log', + owner: + 'RelayReaderAliasedFragmentsTestRequiredBubblesOnAbstractWithMissingTypeInfoQuery', + }, + { + kind: 'missing_required_field.log', + owner: + 'RelayReaderAliasedFragmentsTestRequiredBubblesOnAbstractWithMissingTypeInfoQuery', + fieldPath: 'node.aliased_fragment.name', + }, + ]); expect(isMissingData).toBe(true); expect(data).toEqual({ node: { diff --git a/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js b/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js index 8e9f81bea061..d85cee71c936 100644 --- a/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js @@ -93,7 +93,7 @@ describe('RelayReader @catch', () => { expect(errorResponseFields).toEqual(null); }); - it('if preeceeding scalar sibling has error, catch to RESULT should not catch that error', () => { + it('if preceding scalar sibling has error, catch to RESULT should not catch that error', () => { const source = RelayRecordSource.create({ 'client:root': { __id: 'client:root', @@ -151,7 +151,7 @@ describe('RelayReader @catch', () => { ]); }); - it('if preceeding scalar sibling has a logged missing required field, an THROW required field inside a subsequent @catch should not delete that log', () => { + it('if preceding scalar sibling has a logged missing required field, an THROW required field inside a subsequent @catch should not delete that log', () => { const source = RelayRecordSource.create({ 'client:root': { __id: 'client:root', @@ -173,17 +173,14 @@ describe('RelayReader @catch', () => { lastName @required(action: LOG) } me @catch(to: RESULT) { - # Despite being more destructive, the THOW here should not overwrite + # Despite being more destructive, the THROW here should not overwrite # the LOG, since it gets caught. firstName @required(action: THROW) } } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, errorResponseFields, missingRequiredFields} = read( - source, - operation.fragment, - ); + const {data, errorResponseFields} = read(source, operation.fragment); expect(data).toEqual({ alsoMe: null, me: { @@ -197,16 +194,13 @@ describe('RelayReader @catch', () => { }, }); - expect(errorResponseFields).toEqual(null); - expect(missingRequiredFields).toEqual({ - action: 'LOG', - fields: [ - { - path: 'alsoMe.lastName', - owner: 'RelayReaderCatchFieldsTestSiblingLogRequiredErrorQuery', - }, - ], - }); + expect(errorResponseFields).toEqual([ + { + kind: 'missing_required_field.log', + fieldPath: 'alsoMe.lastName', + owner: 'RelayReaderCatchFieldsTestSiblingLogRequiredErrorQuery', + }, + ]); }); it('@catch(to: NULL) catching a @required(action: THROW) returns null', () => { @@ -232,16 +226,12 @@ describe('RelayReader @catch', () => { } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, errorResponseFields, missingRequiredFields} = read( - source, - operation.fragment, - ); + const {data, errorResponseFields} = read(source, operation.fragment); expect(data).toEqual({ me: null, }); expect(errorResponseFields).toEqual(null); - expect(missingRequiredFields).toEqual(null); }); it('@catch(to: NULL) catching missing data returns null', () => { @@ -267,8 +257,10 @@ describe('RelayReader @catch', () => { } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, errorResponseFields, missingRequiredFields, isMissingData} = - read(source, operation.fragment); + const {data, errorResponseFields, isMissingData} = read( + source, + operation.fragment, + ); // TODO: This should really be: {me: null} expect(data).toEqual({ @@ -281,7 +273,6 @@ describe('RelayReader @catch', () => { expect(isMissingData).toEqual(true); expect(errorResponseFields).toEqual(null); - expect(missingRequiredFields).toEqual(null); }); it('if scalar has catch to RESULT - but no error, response should reflect', () => { @@ -425,10 +416,7 @@ describe('RelayReader @catch', () => { } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, errorResponseFields, missingRequiredFields} = read( - source, - operation.fragment, - ); + const {data, errorResponseFields} = read(source, operation.fragment); expect(data).toEqual({ me: { errors: [ @@ -441,7 +429,6 @@ describe('RelayReader @catch', () => { }, }); - expect(missingRequiredFields).toBeNull(); expect(errorResponseFields).toEqual(null); }); }); diff --git a/packages/relay-runtime/store/__tests__/RelayReader-RequiredFields-test.js b/packages/relay-runtime/store/__tests__/RelayReader-RequiredFields-test.js index b352efb5eb3d..ee15274c2974 100644 --- a/packages/relay-runtime/store/__tests__/RelayReader-RequiredFields-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReader-RequiredFields-test.js @@ -152,9 +152,9 @@ describe('RelayReader @required', () => { } `; const operation = createOperationDescriptor(FooQuery, {id: '1'}); - const {data, missingRequiredFields} = read(source, operation.fragment); + const {data, errorResponseFields} = read(source, operation.fragment); expect(data).toEqual(null); - expect(missingRequiredFields.field.path).toBe('me.lastName'); + expect(errorResponseFields[0].fieldPath).toBe('me.lastName'); }); it('bubbles @required(action: LOG) scalars up to LinkedField even if subsequent fields are not unexpectedly null', () => { @@ -909,7 +909,7 @@ describe('RelayReader @required', () => { const store = new LiveResolverStore(source); const operation = createOperationDescriptor(FooQuery, {}); const resolverCache = new LiveResolverCache(() => source, store); - const {data, missingRequiredFields, errorResponseFields} = read( + const {data, errorResponseFields} = read( source, operation.fragment, resolverCache, @@ -927,7 +927,6 @@ describe('RelayReader @required', () => { }); // these are null because the field with the required error was caught expect(errorResponseFields).toBeNull(); - expect(missingRequiredFields).toBeNull(); }); }); @@ -956,18 +955,18 @@ describe('RelayReader @required', () => { const store = new LiveResolverStore(source); const operation = createOperationDescriptor(FooQuery, {}); const resolverCache = new LiveResolverCache(() => source, store); - const {missingRequiredFields} = read( + const {errorResponseFields} = read( source, operation.fragment, resolverCache, ); - expect(missingRequiredFields).toEqual({ - action: 'THROW', - field: { + expect(errorResponseFields).toEqual([ + { + fieldPath: 'me.client_object', + kind: 'missing_required_field.throw', owner: 'RelayReaderRequiredFieldsTest25Query', - path: 'me.client_object', }, - }); + ]); }); test('does not throw when required field is present', () => { @@ -1001,13 +1000,13 @@ describe('RelayReader @required', () => { const store = new LiveResolverStore(source); const operation = createOperationDescriptor(FooQuery, {}); const resolverCache = new LiveResolverCache(() => source, store); - const {data, missingRequiredFields} = read( + const {data, errorResponseFields} = read( source, operation.fragment, resolverCache, ); expect(data).toEqual({me: {astrological_sign: {name: 'Pisces'}}}); - expect(missingRequiredFields).toBe(null); + expect(errorResponseFields).toBe(null); }); test('does not throw when required plural field is present', () => { @@ -1034,13 +1033,13 @@ describe('RelayReader @required', () => { const store = new LiveResolverStore(source); const operation = createOperationDescriptor(FooQuery, {}); const resolverCache = new LiveResolverCache(() => source, store); - const {data, missingRequiredFields} = read( + const {data, errorResponseFields} = read( source, operation.fragment, resolverCache, ); expect(data.all_astrological_signs.length).toBe(12); - expect(missingRequiredFields).toBe(null); + expect(errorResponseFields).toBe(null); }); test('does not throw when @live required field is suspended', () => { @@ -1063,7 +1062,7 @@ describe('RelayReader @required', () => { const operation = createOperationDescriptor(FooQuery, {}); const resolverCache = new LiveResolverCache(() => source, store); const snapshot = read(source, operation.fragment, resolverCache); - expect(snapshot.missingRequiredFields).toEqual(null); + expect(snapshot.errorResponseFields).toEqual(null); expect(snapshot.missingLiveResolverFields).toEqual([ { path: 'RelayReaderRequiredFieldsTest28Query.live_user_resolver_always_suspend', diff --git a/packages/relay-runtime/store/__tests__/RelayReader-Resolver-test.js b/packages/relay-runtime/store/__tests__/RelayReader-Resolver-test.js index 2099fdc861b1..9f9280ea32f1 100644 --- a/packages/relay-runtime/store/__tests__/RelayReader-Resolver-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReader-Resolver-test.js @@ -245,21 +245,41 @@ describe.each([true, false])( const operation = createOperationDescriptor(FooQuery, {}); const store = new RelayStore(source, {gcReleaseBufferSize: 0}); - const {missingRequiredFields} = store.lookup(operation.fragment); - expect(missingRequiredFields).toEqual({ - action: 'LOG', - fields: [{owner: 'UserRequiredNameResolver', path: 'name'}], - }); + const {errorResponseFields} = store.lookup(operation.fragment); + expect(errorResponseFields).toEqual([ + { + kind: 'missing_required_field.log', + owner: 'UserRequiredNameResolver', + fieldPath: 'name', + }, + { + error: expect.anything(), + fieldPath: 'me.required_name', + kind: 'relay_resolver.error', + owner: 'RelayReaderResolverTestRequiredQuery', + shouldThrow: false, + }, + ]); // Lookup a second time to ensure that we still report the missing fields when // reading from the cache. - const {missingRequiredFields: missingRequiredFieldsTakeTwo} = + const {errorResponseFields: missingRequiredFieldsTakeTwo} = store.lookup(operation.fragment); - expect(missingRequiredFieldsTakeTwo).toEqual({ - action: 'LOG', - fields: [{owner: 'UserRequiredNameResolver', path: 'name'}], - }); + expect(missingRequiredFieldsTakeTwo).toEqual([ + { + kind: 'missing_required_field.log', + owner: 'UserRequiredNameResolver', + fieldPath: 'name', + }, + { + error: expect.anything(), + fieldPath: 'me.required_name', + kind: 'relay_resolver.error', + owner: 'RelayReaderResolverTestRequiredQuery', + shouldThrow: false, + }, + ]); }); it('propagates missing data errors from the resolver up to the reader', () => { @@ -323,33 +343,51 @@ describe.each([true, false])( const operation = createOperationDescriptor(FooQuery, {}); const store = new RelayStore(source, {gcReleaseBufferSize: 0}); - const {missingRequiredFields} = store.lookup(operation.fragment); - expect(missingRequiredFields).toEqual({ - action: 'LOG', - fields: [ - {owner: 'UserRequiredNameResolver', path: 'name'}, - { - owner: 'RelayReaderResolverTestRequiredWithParentQuery', - path: 'me.lastName', - }, - ], - }); + const {errorResponseFields} = store.lookup(operation.fragment); + expect(errorResponseFields).toEqual([ + { + kind: 'missing_required_field.log', + owner: 'UserRequiredNameResolver', + fieldPath: 'name', + }, + { + error: expect.anything(), + fieldPath: 'me.required_name', + kind: 'relay_resolver.error', + owner: 'RelayReaderResolverTestRequiredWithParentQuery', + shouldThrow: false, + }, + { + kind: 'missing_required_field.log', + owner: 'RelayReaderResolverTestRequiredWithParentQuery', + fieldPath: 'me.lastName', + }, + ]); // Lookup a second time to ensure that we still report the missing fields when // reading from the cache. - const {missingRequiredFields: missingRequiredFieldsTakeTwo} = + const {errorResponseFields: missingRequiredFieldsTakeTwo} = store.lookup(operation.fragment); - expect(missingRequiredFieldsTakeTwo).toEqual({ - action: 'LOG', - fields: [ - {owner: 'UserRequiredNameResolver', path: 'name'}, - { - owner: 'RelayReaderResolverTestRequiredWithParentQuery', - path: 'me.lastName', - }, - ], - }); + expect(missingRequiredFieldsTakeTwo).toEqual([ + { + kind: 'missing_required_field.log', + owner: 'UserRequiredNameResolver', + fieldPath: 'name', + }, + { + error: expect.anything(), + fieldPath: 'me.required_name', + kind: 'relay_resolver.error', + owner: 'RelayReaderResolverTestRequiredWithParentQuery', + shouldThrow: false, + }, + { + kind: 'missing_required_field.log', + owner: 'RelayReaderResolverTestRequiredWithParentQuery', + fieldPath: 'me.lastName', + }, + ]); }); it('propagates @required(action: THROW) errors from the resolver up to the reader and avoid calling resolver code', () => { @@ -378,22 +416,28 @@ describe.each([true, false])( const store = new RelayStore(source, {gcReleaseBufferSize: 0}); const beforeCallCount = requiredThrowNameCalls.count; - const {missingRequiredFields} = store.lookup(operation.fragment); + const {errorResponseFields} = store.lookup(operation.fragment); expect(requiredThrowNameCalls.count).toBe(beforeCallCount); - expect(missingRequiredFields).toEqual({ - action: 'THROW', - field: {owner: 'UserRequiredThrowNameResolver', path: 'name'}, - }); + expect(errorResponseFields).toEqual([ + { + kind: 'missing_required_field.throw', + owner: 'UserRequiredThrowNameResolver', + fieldPath: 'name', + }, + ]); // Lookup a second time to ensure that we still report the missing fields when // reading from the cache. - const {missingRequiredFields: missingRequiredFieldsTakeTwo} = + const {errorResponseFields: missingRequiredFieldsTakeTwo} = store.lookup(operation.fragment); - expect(missingRequiredFieldsTakeTwo).toEqual({ - action: 'THROW', - field: {owner: 'UserRequiredThrowNameResolver', path: 'name'}, - }); + expect(missingRequiredFieldsTakeTwo).toEqual([ + { + kind: 'missing_required_field.throw', + owner: 'UserRequiredThrowNameResolver', + fieldPath: 'name', + }, + ]); }); it('works when the field is aliased', () => { diff --git a/packages/relay-runtime/store/__tests__/observeFragment-test.js b/packages/relay-runtime/store/__tests__/observeFragment-test.js index 4253959f1630..328073ce393f 100644 --- a/packages/relay-runtime/store/__tests__/observeFragment-test.js +++ b/packages/relay-runtime/store/__tests__/observeFragment-test.js @@ -24,7 +24,6 @@ const {graphql} = require('../../query/GraphQLTag'); const RelayFeatureFlags = require('../../util/RelayFeatureFlags'); const LiveResolverStore = require('../experimental-live-resolvers/LiveResolverStore'); const {observeFragment} = require('../observeFragmentExperimental'); -const {RelayFieldError} = require('../RelayErrorTrie'); const { createOperationDescriptor, } = require('../RelayModernOperationDescriptor'); @@ -198,7 +197,7 @@ test('Field error with @throwOnFieldError', async () => { withObservableValues(observable, results => { expect(results).toEqual([ { - error: new RelayFieldError( + error: new Error( 'Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors', ), state: 'error', @@ -235,8 +234,8 @@ test('Resolver error with @throwOnFieldError', async () => { withObservableValues(observable, results => { expect(results).toEqual([ { - error: new RelayFieldError( - 'Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors', + error: new Error( + "Relay: Resolver error at path 'always_throws' in 'observeFragmentTestResolverErrorWithThrowOnFieldErrorFragment'.", ), state: 'error', }, diff --git a/packages/relay-runtime/store/__tests__/resolvers/ResolverGC-test.js b/packages/relay-runtime/store/__tests__/resolvers/ResolverGC-test.js index 4aa9b60b2f0e..f38b8d9af3ca 100644 --- a/packages/relay-runtime/store/__tests__/resolvers/ResolverGC-test.js +++ b/packages/relay-runtime/store/__tests__/resolvers/ResolverGC-test.js @@ -179,10 +179,13 @@ test('Regular resolver with fragment reads live resovler with fragment', async ( }, afterLookupAfterFreedGC: (snapshot, recordIdsInStore) => { expect(snapshot.data).toEqual({counter_plus_one: undefined}); - expect(snapshot.missingRequiredFields).toEqual({ - action: 'THROW', - field: {owner: 'CounterPlusOneResolver', path: 'counter'}, - }); + expect(snapshot.errorResponseFields).toEqual([ + { + fieldPath: 'counter', + kind: 'missing_required_field.throw', + owner: 'CounterPlusOneResolver', + }, + ]); expect(recordIdsInStore).toEqual([ 'client:root', 'client:root:counter', diff --git a/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js b/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js index 39ba2281f7f6..fbecb9a84e76 100644 --- a/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js +++ b/packages/relay-runtime/store/__tests__/waitForFragmentData-test.js @@ -88,7 +88,7 @@ test('Promise rejects with @throwOnFieldError', async () => { result = e; } expect(result?.message).toEqual( - 'Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors', + "Relay: Resolver error at path 'always_throws' in 'waitForFragmentDataTestResolverErrorWithThrowOnFieldErrorFragment'.", ); }); @@ -189,6 +189,6 @@ test('data goes missing due to unrelated query response (@throwOnFieldErrro)', a result = e; } expect(result?.message).toEqual( - 'Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors', + "Relay: Missing expected data at path '' in 'waitForFragmentDataTestMissingDataThrowOnFieldErrorFragment'.", ); }); diff --git a/packages/relay-runtime/store/observeFragmentExperimental.js b/packages/relay-runtime/store/observeFragmentExperimental.js index b4451d8e9720..18a200a94a95 100644 --- a/packages/relay-runtime/store/observeFragmentExperimental.js +++ b/packages/relay-runtime/store/observeFragmentExperimental.js @@ -210,12 +210,7 @@ function snapshotToFragmentState( } try { - handlePotentialSnapshotErrors( - environment, - snapshot.missingRequiredFields, - snapshot.errorResponseFields, - snapshot.selector.node.metadata?.throwOnFieldError ?? false, - ); + handlePotentialSnapshotErrors(environment, snapshot.errorResponseFields); } catch (error) { return {error, state: 'error'}; } diff --git a/packages/relay-runtime/util/__tests__/handlePotentialSnapshotErrors-test.js b/packages/relay-runtime/util/__tests__/handlePotentialSnapshotErrors-test.js index 5150670f581e..5777ff875a63 100644 --- a/packages/relay-runtime/util/__tests__/handlePotentialSnapshotErrors-test.js +++ b/packages/relay-runtime/util/__tests__/handlePotentialSnapshotErrors-test.js @@ -25,22 +25,20 @@ describe('handlePotentialSnapshotErrors', () => { it('should not throw in default case', () => { expect(() => { - handlePotentialSnapshotErrors(environment, null, null, false); + handlePotentialSnapshotErrors(environment, null); }).not.toThrow(); }); describe('missing required field handling', () => { it('throws', () => { expect(() => { - handlePotentialSnapshotErrors( - environment, + handlePotentialSnapshotErrors(environment, [ { - action: 'THROW', - field: {owner: 'testOwner', path: 'testPath'}, + kind: 'missing_required_field.throw', + owner: 'testOwner', + fieldPath: 'testPath', }, - null, - false /* throwOnFieldError */, - ); + ]); }).toThrowError( /^Relay: Missing @required value at path 'testPath' in 'testOwner'./, ); @@ -50,18 +48,19 @@ describe('handlePotentialSnapshotErrors', () => { expect(() => { handlePotentialSnapshotErrors( environment, - { - action: 'THROW', - field: {owner: 'testOwner', path: 'testPath'}, - }, + [ + { + kind: 'missing_required_field.throw', + owner: 'testOwner', + fieldPath: 'testPath', + }, { kind: 'missing_expected_data.log', owner: 'RelayModernStoreSubscriptionsTest1Fragment', fieldPath: '', }, ], - false /* throwOnFieldError */, ); }).toThrowError( /^Relay: Missing @required value at path 'testPath' in 'testOwner'./, @@ -70,47 +69,42 @@ describe('handlePotentialSnapshotErrors', () => { it('throws required even when missingData exists in errors array', () => { expect(() => { - handlePotentialSnapshotErrors( - environment, + handlePotentialSnapshotErrors(environment, [ { - action: 'THROW', - field: {owner: 'testOwner', path: 'testPath'}, + kind: 'missing_required_field.throw', + owner: 'testOwner', + fieldPath: 'testPath', }, - [ - { - kind: 'missing_expected_data.log', - owner: 'RelayModernStoreSubscriptionsTest1Fragment', - fieldPath: '', - }, - { - kind: 'relay_field_payload.error', - owner: 'testOwner', - fieldPath: 'testPath', - error: { - message: 'testMessage', - path: ['testPath'], - severity: 'CRITICAL', - }, - shouldThrow: false, + { + kind: 'missing_expected_data.log', + owner: 'RelayModernStoreSubscriptionsTest1Fragment', + fieldPath: '', + }, + { + kind: 'relay_field_payload.error', + owner: 'testOwner', + fieldPath: 'testPath', + error: { + message: 'testMessage', + path: ['testPath'], + severity: 'CRITICAL', }, - ], - false /* throwOnFieldError */, - ); + shouldThrow: false, + }, + ]); }).toThrowError( /^Relay: Missing @required value at path 'testPath' in 'testOwner'./, ); }); it('logs', () => { - handlePotentialSnapshotErrors( - environment, + handlePotentialSnapshotErrors(environment, [ { - action: 'LOG', - fields: [{owner: 'testOwner', path: 'testPath'}], + kind: 'missing_required_field.log', + owner: 'testOwner', + fieldPath: 'testPath', }, - null, - false /* throwOnFieldError */, - ); + ]); expect(relayFieldLogger).toHaveBeenCalledTimes(1); expect(relayFieldLogger).toHaveBeenCalledWith({ @@ -124,21 +118,14 @@ describe('handlePotentialSnapshotErrors', () => { describe('isMissingData field handling', () => { it('throws on throwOnFieldError true', () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'missing_expected_data.throw', - owner: '', - fieldPath: '', - }, - ], - true /* throwOnFieldError */, - ); - }).toThrowError( - /^Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors/, - ); + handlePotentialSnapshotErrors(environment, [ + { + kind: 'missing_expected_data.throw', + owner: '', + fieldPath: '', + }, + ]); + }).toThrowError(/^Relay: Missing expected data at path '' in ''./); expect(relayFieldLogger).toHaveBeenCalledTimes(1); expect(relayFieldLogger).toHaveBeenCalledWith({ @@ -150,18 +137,13 @@ describe('handlePotentialSnapshotErrors', () => { it("logs missing data but doesn't throw when throwOnFieldError is false", () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'missing_expected_data.log', - owner: '', - fieldPath: '', - }, - ], - false /* throwOnFieldError */, - ); + handlePotentialSnapshotErrors(environment, [ + { + kind: 'missing_expected_data.log', + owner: '', + fieldPath: '', + }, + ]); }).not.toThrow(); expect(relayFieldLogger).toHaveBeenCalledTimes(1); @@ -174,18 +156,13 @@ describe('handlePotentialSnapshotErrors', () => { it("logs missing data but doesn't throw when throwOnFieldError is false", () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'missing_expected_data.log', - owner: '', - fieldPath: '', - }, - ], - false /* throwOnFieldError */, - ); + handlePotentialSnapshotErrors(environment, [ + { + kind: 'missing_expected_data.log', + owner: '', + fieldPath: '', + }, + ]); }).not.toThrow(); expect(relayFieldLogger).toHaveBeenCalledTimes(1); @@ -200,41 +177,31 @@ describe('handlePotentialSnapshotErrors', () => { describe('field error handling', () => { it("doesn't throw even when MISSING_DATA exists in errors array", () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'missing_expected_data.log', - owner: '', - fieldPath: '', - }, - ], - false /* throwOnFieldError */, - ); + handlePotentialSnapshotErrors(environment, [ + { + kind: 'missing_expected_data.log', + owner: '', + fieldPath: '', + }, + ]); }).not.toThrow(); }); it("only logs but doesn't throw when explicit error handling disabled", () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'relay_field_payload.error', - owner: 'testOwner', - fieldPath: 'testPath', - error: { - message: 'testMessage', - path: ['testPath'], - severity: 'CRITICAL', - }, - shouldThrow: false, + handlePotentialSnapshotErrors(environment, [ + { + kind: 'relay_field_payload.error', + owner: 'testOwner', + fieldPath: 'testPath', + error: { + message: 'testMessage', + path: ['testPath'], + severity: 'CRITICAL', }, - ], - false /* throwOnFieldError */, - ); + shouldThrow: false, + }, + ]); }).not.toThrow(); expect(relayFieldLogger).toHaveBeenCalledTimes(1); @@ -253,29 +220,24 @@ describe('handlePotentialSnapshotErrors', () => { it("only logs twice but doesn't throw when explicit error handling disabled - and missing data", () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'relay_field_payload.error', - owner: 'testOwner', - fieldPath: 'testPath', - error: { - message: 'testMessage', - path: ['testPath'], - severity: 'CRITICAL', - }, - shouldThrow: false, - }, - { - kind: 'missing_expected_data.log', - owner: '', - fieldPath: '', + handlePotentialSnapshotErrors(environment, [ + { + kind: 'relay_field_payload.error', + owner: 'testOwner', + fieldPath: 'testPath', + error: { + message: 'testMessage', + path: ['testPath'], + severity: 'CRITICAL', }, - ], - false /* throwOnFieldError */, - ); + shouldThrow: false, + }, + { + kind: 'missing_expected_data.log', + owner: '', + fieldPath: '', + }, + ]); }).not.toThrow(); expect(relayFieldLogger).toHaveBeenCalledTimes(2); @@ -298,24 +260,19 @@ describe('handlePotentialSnapshotErrors', () => { }); it('logs', () => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'relay_field_payload.error', - owner: 'testOwner', - fieldPath: 'testPath', - error: { - message: 'testMessage', - path: ['testPath'], - severity: 'CRITICAL', - }, - shouldThrow: false, + handlePotentialSnapshotErrors(environment, [ + { + kind: 'relay_field_payload.error', + owner: 'testOwner', + fieldPath: 'testPath', + error: { + message: 'testMessage', + path: ['testPath'], + severity: 'CRITICAL', }, - ], - false /* throwOnFieldError */, - ); + shouldThrow: false, + }, + ]); expect(relayFieldLogger).toHaveBeenCalledTimes(1); expect(relayFieldLogger).toHaveBeenCalledWith({ @@ -334,29 +291,24 @@ describe('handlePotentialSnapshotErrors', () => { it('throws when throwOnFieldError enabled', () => { expect(() => { // in this case, the MISSING_DATA error is thrown *with* the others - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'relay_field_payload.error', - owner: 'testOwner', - fieldPath: 'testPath', - error: { - message: 'testMessage', - path: ['testPath'], - severity: 'CRITICAL', - }, - shouldThrow: true, - }, - { - kind: 'missing_expected_data.log', - owner: 'RelayModernStoreSubscriptionsTest1Fragment', - fieldPath: '', + handlePotentialSnapshotErrors(environment, [ + { + kind: 'relay_field_payload.error', + owner: 'testOwner', + fieldPath: 'testPath', + error: { + message: 'testMessage', + path: ['testPath'], + severity: 'CRITICAL', }, - ], - true /* throwOnFieldError */, - ); + shouldThrow: true, + }, + { + kind: 'missing_expected_data.log', + owner: 'RelayModernStoreSubscriptionsTest1Fragment', + fieldPath: '', + }, + ]); }).toThrowError( /^Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors/, ); @@ -386,20 +338,15 @@ describe('handlePotentialSnapshotErrors', () => { describe('resolver error handling', () => { it('logs when explicit error handling disabled', () => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'relay_resolver.error', - fieldPath: 'testPath', - owner: 'testOwner', - error: Error('testError'), - shouldThrow: false, - }, - ], - false /* throwOnFieldError */, - ); + handlePotentialSnapshotErrors(environment, [ + { + kind: 'relay_resolver.error', + fieldPath: 'testPath', + owner: 'testOwner', + error: Error('testError'), + shouldThrow: false, + }, + ]); expect(relayFieldLogger).toHaveBeenCalledTimes(1); expect(relayFieldLogger).toHaveBeenCalledWith({ @@ -413,22 +360,17 @@ describe('handlePotentialSnapshotErrors', () => { it('throws when explicit error handling enabled', () => { expect(() => { - handlePotentialSnapshotErrors( - environment, - null, - [ - { - kind: 'relay_resolver.error', - fieldPath: 'testPath', - owner: 'testOwner', - error: Error('testError'), - shouldThrow: true, - }, - ], - true /* throwOnFieldError */, - ); + handlePotentialSnapshotErrors(environment, [ + { + kind: 'relay_resolver.error', + fieldPath: 'testPath', + owner: 'testOwner', + error: Error('testError'), + shouldThrow: true, + }, + ]); }).toThrowError( - /^Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors/, + /^Relay: Resolver error at path 'testPath' in 'testOwner'/, ); expect(relayFieldLogger).toHaveBeenCalledTimes(1); diff --git a/packages/relay-runtime/util/handlePotentialSnapshotErrors.js b/packages/relay-runtime/util/handlePotentialSnapshotErrors.js index a5df8b39922f..076c7546d093 100644 --- a/packages/relay-runtime/util/handlePotentialSnapshotErrors.js +++ b/packages/relay-runtime/util/handlePotentialSnapshotErrors.js @@ -11,19 +11,17 @@ 'use strict'; -import type {TRelayFieldErrorForDisplay} from '../store/RelayErrorTrie'; import type { + ErrorResponseField, ErrorResponseFields, IEnvironment, - MissingRequiredFields, } from '../store/RelayStoreTypes'; -const {RelayFieldError} = require('../store/RelayErrorTrie'); +const invariant = require('invariant'); function handleFieldErrors( environment: IEnvironment, errorResponseFields: ErrorResponseFields, - shouldThrow: boolean, ) { for (const fieldError of errorResponseFields) { // First we log all events. Note that the logger may opt to throw its own @@ -32,79 +30,66 @@ function handleFieldErrors( environment.relayFieldLogger(fieldError); } - // when a user adds the throwOnFieldError flag, they opt into also throwing on missing fields. - if (shouldThrow) { - throw new RelayFieldError( - `Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors`, - errorResponseFields.map((event): TRelayFieldErrorForDisplay => { - switch (event.kind) { - case 'relay_field_payload.error': - //TODO: [relay] Provide a payloadErrorResolver to allow exposing custom error shape. - const {message, ...displayError} = event.error; - return displayError; - case 'missing_expected_data.throw': - return {path: event.fieldPath.split('.')}; - case 'missing_expected_data.log': - return {path: event.fieldPath.split('.')}; - case 'relay_resolver.error': - return {path: event.fieldPath.split('.')}; - default: - (event.kind: empty); - throw new Error('Relay: Unexpected event kind'); - } - }), - ); + for (const fieldError of errorResponseFields) { + if (eventShouldThrow(fieldError)) { + switch (fieldError.kind) { + case 'relay_resolver.error': + throw new Error( + `Relay: Resolver error at path '${fieldError.fieldPath}' in '${fieldError.owner}'.`, + ); + case 'relay_field_payload.error': + throw new Error( + `Relay: Unexpected response payload - this object includes an errors property in which you can access the underlying errors`, + ); + case 'missing_expected_data.throw': + throw new Error( + `Relay: Missing expected data at path '${fieldError.fieldPath}' in '${fieldError.owner}'.`, + ); + case 'missing_required_field.throw': + throw new Error( + `Relay: Missing @required value at path '${fieldError.fieldPath}' in '${fieldError.owner}'.`, + ); + case 'missing_required_field.log': + case 'missing_expected_data.log': + // These should have already been filtered out. Sadly, Flow Type + // Guards don't work well with refining discriminated unions, so we + // can't assert this via types. + break; + default: + (fieldError.kind: empty); + invariant(false, 'Relay: Unexpected event kind: %s', fieldError.kind); + } + } } } -function handleMissingRequiredFields( - environment: IEnvironment, - missingRequiredFields: MissingRequiredFields, -) { - switch (missingRequiredFields.action) { - case 'THROW': { - const {path, owner} = missingRequiredFields.field; - // This gives the consumer the chance to throw their own error if they so wish. - environment.relayFieldLogger({ - kind: 'missing_required_field.throw', - owner, - fieldPath: path, - }); - throw new Error( - `Relay: Missing @required value at path '${path}' in '${owner}'.`, - ); - } - case 'LOG': - missingRequiredFields.fields.forEach(({path, owner}) => { - environment.relayFieldLogger({ - kind: 'missing_required_field.log', - owner, - fieldPath: path, - }); - }); - break; - default: { - (missingRequiredFields.action: empty); - } +function eventShouldThrow(event: ErrorResponseField): boolean { + switch (event.kind) { + case 'relay_resolver.error': + case 'relay_field_payload.error': + return event.shouldThrow; + case 'missing_expected_data.throw': + case 'missing_required_field.throw': + return true; + case 'missing_required_field.log': + case 'missing_expected_data.log': + return false; + default: + (event.kind: empty); + throw new Error('Relay: Unexpected event kind'); } } function handlePotentialSnapshotErrors( environment: IEnvironment, - missingRequiredFields: ?MissingRequiredFields, errorResponseFields: ?ErrorResponseFields, - throwOnFieldError: boolean, ) { - if (missingRequiredFields != null) { - handleMissingRequiredFields(environment, missingRequiredFields); - } - /** * Inside handleFieldErrors, we check for throwOnFieldError - but this fn logs the error anyway by default * which is why this still should run in any case there's errors. */ if (errorResponseFields != null) { - handleFieldErrors(environment, errorResponseFields, throwOnFieldError); + handleFieldErrors(environment, errorResponseFields); } }