Skip to content

Commit

Permalink
Support @catch directive in RelayReader
Browse files Browse the repository at this point in the history
Summary:
This is initial support for catch - it adds the appropriate errors to the snapshot.

In case of a caught required, it deletes the original required error - and moves it over to a to:RESULT error in fieldErrors.

This does not include providing the error in hooks and such yet.

This is the initial preparation for exposing the result in a catch manner rather than throwing.

Reviewed By: tyao1

Differential Revision: D54806505

fbshipit-source-id: c0b3884fcde1e9f9c7f9ac4e37eef8842588e3d1
  • Loading branch information
itamark authored and facebook-github-bot committed Apr 15, 2024
1 parent 7b69061 commit 696a1b3
Show file tree
Hide file tree
Showing 10 changed files with 781 additions and 15 deletions.
2 changes: 2 additions & 0 deletions packages/relay-runtime/mutations/createUpdatableProxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
ACTOR_CHANGE,
ALIASED_FRAGMENT_SPREAD,
ALIASED_INLINE_FRAGMENT_SPREAD,
CATCH_FIELD,
CLIENT_EDGE_TO_CLIENT_OBJECT,
CLIENT_EDGE_TO_SERVER_OBJECT,
CLIENT_EXTENSION,
Expand Down Expand Up @@ -204,6 +205,7 @@ function updateProxyFromSelections<TData>(
case MODULE_IMPORT:
case RELAY_LIVE_RESOLVER:
case REQUIRED_FIELD:
case CATCH_FIELD:
case STREAM:
case RELAY_RESOLVER:
// These types of reader nodes are not currently handled.
Expand Down
110 changes: 96 additions & 14 deletions packages/relay-runtime/store/RelayReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import type {
ReaderActorChange,
ReaderAliasedFragmentSpread,
ReaderCatchField,
ReaderClientEdge,
ReaderFragment,
ReaderFragmentSpread,
Expand Down Expand Up @@ -51,6 +52,7 @@ const {
ACTOR_CHANGE,
ALIASED_FRAGMENT_SPREAD,
ALIASED_INLINE_FRAGMENT_SPREAD,
CATCH_FIELD,
CLIENT_EDGE_TO_CLIENT_OBJECT,
CLIENT_EDGE_TO_SERVER_OBJECT,
CLIENT_EXTENSION,
Expand Down Expand Up @@ -93,6 +95,8 @@ const {
const {generateTypeID} = require('./TypeID');
const invariant = require('invariant');

type RequiredOrCatchField = ReaderRequiredField | ReaderCatchField;

function read(
recordSource: RecordSource,
selector: SingularReaderSelector,
Expand Down Expand Up @@ -236,6 +240,7 @@ class RelayReader {
if (!RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING) {
return;
}

const errors = RelayModernRecord.getErrors(record, storageKey);

if (errors == null) {
Expand Down Expand Up @@ -282,7 +287,6 @@ class RelayReader {
): ?SelectorData {
const record = this._recordSource.get(dataID);
this._seenRecords.add(dataID);

if (record == null) {
if (record === undefined) {
this._markDataAsMissing();
Expand Down Expand Up @@ -341,28 +345,106 @@ class RelayReader {
}
}

_handleCatchFieldValue(selection: ReaderCatchField) {
const {to} = selection;

if (this._errorResponseFields != null) {
for (let i = 0; i < this._errorResponseFields.length; i++) {
// if it's a @catch - it can only be NULL or RESULT. So we always add the "to" from the CatchField.
this._errorResponseFields[i].to = to;
}
}
// If we have a nested @required(THROW) that will throw,
// we want to catch that error and provide it, and remove the original error
if (this._missingRequiredFields?.action === 'THROW') {
if (this._missingRequiredFields?.field == null) {
return;
}

// We want to catch nested @required THROWs
if (this._errorResponseFields == null) {
this._errorResponseFields = [];
}

const {owner, path} = this._missingRequiredFields.field;
this._errorResponseFields.push({
owner: owner,
path: path,
error: {
message: `Relay: Missing @required value at path '${path}' in '${owner}'.`,
},
to,
});

// remove missing required because we're providing it in catch instead.
this._missingRequiredFields = null;

return;
}

// we do nothing if to is 'NULL'
}

_handleRequiredFieldValue(
selection: ReaderRequiredField,
value: mixed,
): boolean /*should continue to siblings*/ {
if (value == null) {
const {action} = selection;
if (action !== 'NONE') {
this._maybeReportUnexpectedNull(selection.path, action);
}
// We are going to throw, or our parent is going to get nulled out.
// Either way, sibling values are going to be ignored, so we can
// bail early here as an optimization.
return false;
}
return true;
}

_isRequiredField(
selection: RequiredOrCatchField,
): selection is ReaderRequiredField {
return selection.kind === REQUIRED_FIELD;
}

_isCatchField(
selection: RequiredOrCatchField,
): selection is ReaderCatchField {
return selection.kind === CATCH_FIELD;
}

_traverseSelections(
selections: $ReadOnlyArray<ReaderSelection>,
record: Record,
data: SelectorData,
): boolean /* had all expected data */ {
for (let i = 0; i < selections.length; i++) {
const selection = selections[i];

switch (selection.kind) {
case REQUIRED_FIELD: {
const fieldValue = this._readRequiredField(selection, record, data);
if (fieldValue == null) {
const {action} = selection;
if (action !== 'NONE') {
this._maybeReportUnexpectedNull(selection.path, action);
}
// We are going to throw, or our parent is going to get nulled out.
// Either way, sibling values are going to be ignored, so we can
// bail early here as an optimization.
case REQUIRED_FIELD:
const requiredFieldValue = this._readClientSideDirectiveField(
selection,
record,
data,
);
if (!this._handleRequiredFieldValue(selection, requiredFieldValue)) {
return false;
}
break;
}
case CATCH_FIELD:
this._readClientSideDirectiveField(selection, record, data);
if (RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE) {
/* NULL is old behavior. do nothing. */
if (selection.to != 'NULL') {
/* @catch(to: RESULT) is the default */
this._handleCatchFieldValue(selection);
}
}

break;

case SCALAR_FIELD:
this._readScalar(selection, record, data);
break;
Expand Down Expand Up @@ -494,8 +576,8 @@ class RelayReader {
return true;
}

_readRequiredField(
selection: ReaderRequiredField,
_readClientSideDirectiveField(
selection: RequiredOrCatchField,
record: Record,
data: SelectorData,
): ?mixed {
Expand Down
2 changes: 2 additions & 0 deletions packages/relay-runtime/store/RelayStoreTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
NormalizationSelectableNode,
} from '../util/NormalizationNode';
import type {
CatchFieldTo,
ReaderClientEdgeToServerObject,
ReaderFragment,
ReaderLinkedField,
Expand Down Expand Up @@ -119,6 +120,7 @@ type FieldLocation = {
type ErrorFieldLocation = {
...FieldLocation,
error: TRelayFieldError,
to?: CatchFieldTo,
};

export type MissingRequiredFields = $ReadOnly<
Expand Down
Loading

0 comments on commit 696a1b3

Please sign in to comment.