diff --git a/packages/relay-runtime/mutations/createUpdatableProxy.js b/packages/relay-runtime/mutations/createUpdatableProxy.js index cc7502402c860..32000fa879e1e 100644 --- a/packages/relay-runtime/mutations/createUpdatableProxy.js +++ b/packages/relay-runtime/mutations/createUpdatableProxy.js @@ -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, @@ -204,6 +205,7 @@ function updateProxyFromSelections( 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. diff --git a/packages/relay-runtime/store/RelayReader.js b/packages/relay-runtime/store/RelayReader.js index b3666268853ad..6b52fa45f980d 100644 --- a/packages/relay-runtime/store/RelayReader.js +++ b/packages/relay-runtime/store/RelayReader.js @@ -14,6 +14,7 @@ import type { ReaderActorChange, ReaderAliasedFragmentSpread, + ReaderCatchField, ReaderClientEdge, ReaderFragment, ReaderFragmentSpread, @@ -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, @@ -93,6 +95,8 @@ const { const {generateTypeID} = require('./TypeID'); const invariant = require('invariant'); +type RequiredOrCatchField = ReaderRequiredField | ReaderCatchField; + function read( recordSource: RecordSource, selector: SingularReaderSelector, @@ -236,6 +240,7 @@ class RelayReader { if (!RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING) { return; } + const errors = RelayModernRecord.getErrors(record, storageKey); if (errors == null) { @@ -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(); @@ -341,6 +345,75 @@ 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, record: Record, @@ -348,21 +421,30 @@ class RelayReader { ): 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; @@ -494,8 +576,8 @@ class RelayReader { return true; } - _readRequiredField( - selection: ReaderRequiredField, + _readClientSideDirectiveField( + selection: RequiredOrCatchField, record: Record, data: SelectorData, ): ?mixed { diff --git a/packages/relay-runtime/store/RelayStoreTypes.js b/packages/relay-runtime/store/RelayStoreTypes.js index 3a1287dcb1628..ffdcbff0d76b0 100644 --- a/packages/relay-runtime/store/RelayStoreTypes.js +++ b/packages/relay-runtime/store/RelayStoreTypes.js @@ -32,6 +32,7 @@ import type { NormalizationSelectableNode, } from '../util/NormalizationNode'; import type { + CatchFieldTo, ReaderClientEdgeToServerObject, ReaderFragment, ReaderLinkedField, @@ -119,6 +120,7 @@ type FieldLocation = { type ErrorFieldLocation = { ...FieldLocation, error: TRelayFieldError, + to?: CatchFieldTo, }; export type MissingRequiredFields = $ReadOnly< diff --git a/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js b/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js new file mode 100644 index 0000000000000..72afd5e1ddc6d --- /dev/null +++ b/packages/relay-runtime/store/__tests__/RelayReader-CatchFields-test.js @@ -0,0 +1,318 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall relay + */ + +const RelayFeatureFlags = require('../../util/RelayFeatureFlags'); +const { + createOperationDescriptor, +} = require('../RelayModernOperationDescriptor'); +const {read} = require('../RelayReader'); +const RelayRecordSource = require('../RelayRecordSource'); +const RelayReaderCatchFieldsTest0Query = require('./__mocks__/RelayReaderCatchFieldsTest0Query.graphql.js'); +const RelayReaderCatchFieldsTest1Query = require('./__mocks__/RelayReaderCatchFieldsTest1Query.graphql.js'); +const RelayReaderCatchFieldsTest2Query = require('./__mocks__/RelayReaderCatchFieldsTest2Query.graphql.js'); + +describe('RelayReader @catch', () => { + describe('when catch is enabled', () => { + beforeAll(() => { + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING = true; + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE = true; + }); + + const wasFieldErrorHandlingEnabled = + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING; + const wasCatchEnabled = + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE; + it('if scalar has @catch(to: NULL) - scalar value should be null, and nothing should throw or catch', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + me: {__ref: '1'}, + }, + '1': { + __id: '1', + id: '1', + __typename: 'User', + lastName: null, + }, + }); + + // Mocking the query below with RelayReaderCatchFieldsTest0Query + // const FooQuery = graphql` + // query RelayReaderCatchFieldsTest0Query { + // me { + // lastName @catch(to: NULL) + // } + // } + // `; + const operation = createOperationDescriptor( + RelayReaderCatchFieldsTest0Query, + {id: '1'}, + ); + const {data} = read(source, operation.fragment); + expect(data).toEqual({me: {lastName: null}}); + }); + + it('if scalar has @catch(to: RESULT) - scalar value should provide the error', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + me: {__ref: '1'}, + }, + '1': { + __id: '1', + id: '1', + __typename: 'User', + lastName: null, + __errors: { + lastName: [ + { + message: 'There was an error!', + path: ['me', 'lastName'], + }, + ], + }, + }, + }); + + // Mocking the query below with RelayReaderCatchFieldsTest0Query + // const FooQuery = graphql` + // query RelayReaderCatchFieldsTest1Query { + // me { + // lastName @catch(to: RESULT) + // } + // } + // `; + const operation = createOperationDescriptor( + RelayReaderCatchFieldsTest1Query, + {id: '1'}, + ); + const {data, errorResponseFields} = read(source, operation.fragment); + expect(data).toEqual({ + me: { + lastName: null, + }, + }); + + expect(errorResponseFields).toEqual([ + { + path: 'me.lastName', + to: 'RESULT', + error: { + message: 'There was an error!', + path: ['me', 'lastName'], + }, + owner: 'RelayReaderCatchFieldsTest1Query', + }, + ]); + }); + + it('if scalar has catch to RESULT with nested required', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + me: {__ref: '1'}, + }, + '1': { + __id: '1', + id: '1', + __typename: 'User', + lastName: null, + }, + }); + + // Mocking the query below with RelayReaderCatchFieldsTest0Query + // const FooQuery = graphql` + // query RelayReaderCatchFieldsTest1Query { + // me @catch { + // lastName @required(action: THROW) + // } + // } + // `; + const operation = createOperationDescriptor( + RelayReaderCatchFieldsTest2Query, + {id: '1'}, + ); + const {data, errorResponseFields, missingRequiredFields} = read( + source, + operation.fragment, + ); + expect(data).toEqual({ + me: null, + }); + + expect(missingRequiredFields).toBeNull(); + expect(errorResponseFields).toEqual([ + { + owner: 'RelayReaderCatchFieldsTest2Query', + path: 'me.lastName', + error: { + message: + "Relay: Missing @required value at path 'me.lastName' in 'RelayReaderCatchFieldsTest2Query'.", + }, + to: 'RESULT', + }, + ]); + }); + afterAll(() => { + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING = + wasFieldErrorHandlingEnabled; + + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE = + wasCatchEnabled; + }); + }); + describe('when catch is disabled', () => { + beforeAll(() => { + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING = true; + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE = false; + }); + + const wasFieldErrorHandlingEnabled = + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING; + const wasCatchEnabled = + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE; + it('if scalar has @catch(to: NULL) - scalar value should be null, and nothing should throw or catch', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + me: {__ref: '1'}, + }, + '1': { + __id: '1', + id: '1', + __typename: 'User', + lastName: null, + }, + }); + + // Mocking the query below with RelayReaderCatchFieldsTest0Query + // const FooQuery = graphql` + // query RelayReaderCatchFieldsTest0Query { + // me { + // lastName @catch(to: NULL) + // } + // } + // `; + const operation = createOperationDescriptor( + RelayReaderCatchFieldsTest0Query, + {id: '1'}, + ); + const {data} = read(source, operation.fragment); + expect(data).toEqual({me: {lastName: null}}); + }); + + it('if scalar has @catch(to: RESULT) - scalar value should provide the value as a CatchField object', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + me: {__ref: '1'}, + }, + '1': { + __id: '1', + id: '1', + __typename: 'User', + lastName: null, + __errors: { + lastName: [ + { + message: 'There was an error!', + path: ['me', 'lastName'], + }, + ], + }, + }, + }); + + // Mocking the query below with RelayReaderCatchFieldsTest0Query + // const FooQuery = graphql` + // query RelayReaderCatchFieldsTest1Query { + // me { + // lastName @catch(to: RESULT) + // } + // } + // `; + const operation = createOperationDescriptor( + RelayReaderCatchFieldsTest1Query, + {id: '1'}, + ); + const {data, errorResponseFields} = read(source, operation.fragment); + expect(data).toEqual({ + me: { + lastName: null, + }, + }); + + expect(errorResponseFields).toEqual([ + { + path: 'me.lastName', + error: { + message: 'There was an error!', + path: ['me', 'lastName'], + }, + owner: 'RelayReaderCatchFieldsTest1Query', + }, + ]); + }); + + it('if scalar has catch to RESULT with nested required', () => { + const source = RelayRecordSource.create({ + 'client:root': { + __id: 'client:root', + __typename: '__Root', + me: {__ref: '1'}, + }, + '1': { + __id: '1', + id: '1', + __typename: 'User', + lastName: null, + }, + }); + + // Mocking the query below with RelayReaderCatchFieldsTest0Query + // const FooQuery = graphql` + // query RelayReaderCatchFieldsTest1Query { + // me @catch { + // lastName @required(action: THROW) + // } + // } + // `; + const operation = createOperationDescriptor( + RelayReaderCatchFieldsTest2Query, + {id: '1'}, + ); + const {data, errorResponseFields, missingRequiredFields} = read( + source, + operation.fragment, + ); + expect(data).toEqual({ + me: null, + }); + expect(missingRequiredFields).toEqual({ + action: 'THROW', + field: {owner: 'RelayReaderCatchFieldsTest2Query', path: 'me.lastName'}, + }); + expect(errorResponseFields).toBeNull(); + }); + afterAll(() => { + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING = + wasFieldErrorHandlingEnabled; + + RelayFeatureFlags.ENABLE_FIELD_ERROR_HANDLING_CATCH_DIRECTIVE = + wasCatchEnabled; + }); + }); +}); diff --git a/packages/relay-runtime/store/__tests__/RelayReader-RelayErrorHandling-test.js b/packages/relay-runtime/store/__tests__/RelayReader-RelayErrorHandling-test.js index bb70c06d5b957..314d5dbdd7a63 100644 --- a/packages/relay-runtime/store/__tests__/RelayReader-RelayErrorHandling-test.js +++ b/packages/relay-runtime/store/__tests__/RelayReader-RelayErrorHandling-test.js @@ -64,7 +64,10 @@ describe('RelayReader error fields', () => { { owner: 'RelayReaderRelayErrorHandlingTest1Query', path: 'me.lastName', - error: {message: 'There was an error!', path: ['me', 'lastName']}, + error: { + message: 'There was an error!', + path: ['me', 'lastName'], + }, }, ]); }); diff --git a/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest0Query.graphql.js b/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest0Query.graphql.js new file mode 100644 index 0000000000000..3758b46305fa4 --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest0Query.graphql.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +export type RelayReaderCatchFieldsTest0Query$variables = {||}; +export type RelayReaderCatchFieldsTest0Query$data = {| + +me: {| + +lastName: string, + |}, +|}; +export type RelayReaderCatchFieldsTest0Query = {| + response: RelayReaderCatchFieldsTest0Query$data, + variables: RelayReaderCatchFieldsTest0Query$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastName", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayReaderCatchFieldsTest0Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "kind": "CatchField", + "field": (v0/*: any*/), + "to": "NULL", + "path": "me.lastName" + } + ], + "storageKey": null, + }, + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "RelayReaderCatchFieldsTest0Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8cd69a31b3db9176dc76e43d3a795c6f", + "id": null, + "metadata": {}, + "name": "RelayReaderCatchFieldsTest0Query", + "operationKind": "query", + "text": "query RelayReaderCatchFieldsTest0Query {\n me {\n lastName\n id\n }\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "87b6ffdc922687a788965139fef7a707"; +} + +module.exports = ((node/*: any*/)/*: Query< + RelayReaderCatchFieldsTest0Query$variables, + RelayReaderCatchFieldsTest0Query$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest1Query.graphql.js b/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest1Query.graphql.js new file mode 100644 index 0000000000000..57c3ab58198ad --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest1Query.graphql.js @@ -0,0 +1,112 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +export type RelayReaderCatchFieldsTest1Query$variables = {||}; +export type RelayReaderCatchFieldsTest1Query$data = {| + +me: {| + +lastName: string, + |}, +|}; +export type RelayReaderCatchFieldsTest1Query = {| + response: RelayReaderCatchFieldsTest1Query$data, + variables: RelayReaderCatchFieldsTest1Query$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastName", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayReaderCatchFieldsTest1Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "kind": "CatchField", + "field": (v0/*: any*/), + "to": "RESULT", + "path": "me.lastName" + } + ], + "storageKey": null, + }, + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "RelayReaderCatchFieldsTest1Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8cd69a31b3db9176dc76e43d3a795c6f", + "id": null, + "metadata": {}, + "name": "RelayReaderCatchFieldsTest1Query", + "operationKind": "query", + "text": "query RelayReaderCatchFieldsTest1Query {\n me {\n lastName\n id\n }\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "87b6ffdc922687a788965139fef7a707"; +} + +module.exports = ((node/*: any*/)/*: Query< + RelayReaderCatchFieldsTest1Query$variables, + RelayReaderCatchFieldsTest1Query$data, +>*/); diff --git a/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest2Query.graphql.js b/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest2Query.graphql.js new file mode 100644 index 0000000000000..943305c1a81be --- /dev/null +++ b/packages/relay-runtime/store/__tests__/__mocks__/RelayReaderCatchFieldsTest2Query.graphql.js @@ -0,0 +1,121 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @oncall relay + * @flow + * @lightSyntaxTransform + * @nogrep + */ + +/* eslint-disable */ + +'use strict'; + +/*:: +import type { ConcreteRequest, Query } from 'relay-runtime'; +export type RelayReaderCatchFieldsTest2Query$variables = {||}; +export type RelayReaderCatchFieldsTest2Query$data = {| + +me: {| + +lastName: string, + |}, +|}; +export type RelayReaderCatchFieldsTest2Query = {| + response: RelayReaderCatchFieldsTest2Query$data, + variables: RelayReaderCatchFieldsTest2Query$variables, +|}; +*/ + +var node/*: ConcreteRequest*/ = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "lastName", + "storageKey": null +}; + +var meObj = { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "RequiredField", + "field": (v0/*: any*/), + "action": "THROW", + "path": "me.lastName" + } + ], + "storageKey": null, + } +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "RelayReaderCatchFieldsTest2Query", + "selections": [ + { + "kind": "CatchField", + "field": (meObj/*: any*/), + "to": "RESULT", + "path": "me" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "RelayReaderCatchFieldsTest2Query", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "me", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8cd69a31b3db9176dc76e43d3a795c6f", + "id": null, + "metadata": {}, + "name": "RelayReaderCatchFieldsTest2Query", + "operationKind": "query", + "text": "query RelayReaderCatchFieldsTest2Query {\n me {\n lastName\n id\n }\n}\n" + } +}; +})(); + +if (__DEV__) { + (node/*: any*/).hash = "87b6ffdc922687a788965139fef7a707"; +} + +module.exports = ((node/*: any*/)/*: Query< + RelayReaderCatchFieldsTest2Query$variables, + RelayReaderCatchFieldsTest2Query$data, +>*/); diff --git a/packages/relay-runtime/util/ReaderNode.js b/packages/relay-runtime/util/ReaderNode.js index e8a4120526254..78f361b2c40d7 100644 --- a/packages/relay-runtime/util/ReaderNode.js +++ b/packages/relay-runtime/util/ReaderNode.js @@ -236,6 +236,18 @@ export type ReaderRequiredField = { +path: string, }; +export type CatchFieldTo = 'RESULT' | 'NULL'; + +export type ReaderCatchField = { + +kind: 'CatchField', + +field: + | ReaderField + | ReaderClientEdgeToClientObject + | ReaderClientEdgeToServerObject, + +to: CatchFieldTo, + +path: string, +}; + export type ResolverFunction = (...args: Array) => mixed; // flowlint-line unclear-type:off // With ES6 imports, a resolver function might be exported under the `default` key. export type ResolverModule = ResolverFunction | {default: ResolverFunction}; @@ -320,6 +332,7 @@ export type ReaderSelection = | ReaderInlineFragment | ReaderModuleImport | ReaderStream + | ReaderCatchField | ReaderRequiredField | ReaderRelayResolver; diff --git a/packages/relay-runtime/util/RelayConcreteNode.js b/packages/relay-runtime/util/RelayConcreteNode.js index 07a1adddec455..a6d900f3989c6 100644 --- a/packages/relay-runtime/util/RelayConcreteNode.js +++ b/packages/relay-runtime/util/RelayConcreteNode.js @@ -95,6 +95,7 @@ export type GeneratedNode = const RelayConcreteNode = { ACTOR_CHANGE: 'ActorChange', + CATCH_FIELD: 'CatchField', CONDITION: 'Condition', CLIENT_COMPONENT: 'ClientComponent', CLIENT_EDGE_TO_SERVER_OBJECT: 'ClientEdgeToServerObject',