diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21af31cf..a811e684 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
# master
+- Add support for the new Relay `@catch` directive. https://github.com/zth/rescript-relay/pull/549
+
# 3.1.0
This brings the Relay version to `18.2.0`.
diff --git a/packages/relay b/packages/relay
index 00960d5f..d123685c 160000
--- a/packages/relay
+++ b/packages/relay
@@ -1 +1 @@
-Subproject commit 00960d5f9e8e67e2dc304554ac5bf75a878972a6
+Subproject commit d123685cea0e193ae2ff394a081b6871caff0c61
diff --git a/packages/rescript-relay/__tests__/Test_catch-tests.js b/packages/rescript-relay/__tests__/Test_catch-tests.js
new file mode 100644
index 00000000..a8741342
--- /dev/null
+++ b/packages/rescript-relay/__tests__/Test_catch-tests.js
@@ -0,0 +1,184 @@
+require("@testing-library/jest-dom/extend-expect");
+const t = require("@testing-library/react");
+const React = require("react");
+const queryMock = require("./queryMock");
+
+const { test_catch } = require("./Test_catch.bs");
+
+const date = new Date("2025-01-01T06:00");
+
+describe("Catch", () => {
+ test("logged in user prop - success", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchLoggedInUserPropQuery",
+ data: {
+ loggedInUser: {
+ id: "user-1",
+ createdAt: date.toISOString(),
+ },
+ },
+ });
+
+ t.render(test_catch("TestLoggedInUserProp"));
+ await t.screen.findByText("Got createdAt: 2025-01-01");
+ });
+
+ test("logged in user prop - fail", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchLoggedInUserPropQuery",
+ data: {
+ loggedInUser: {
+ id: "user-1",
+ createdAt: null,
+ },
+ },
+ graphqlErrors: [{ path: ["loggedInUser", "createdAt"] }],
+ });
+
+ t.render(test_catch("TestLoggedInUserProp"));
+ await t.screen.findByText("Error!");
+ });
+
+ test("logged in user prop from fragment - success", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchLoggedInUserPropQuery",
+ data: {
+ loggedInUser: {
+ id: "user-1",
+ createdAt: date.toISOString(),
+ },
+ },
+ });
+
+ t.render(test_catch("TestLoggedInUserPropFragmentData"));
+ await t.screen.findByText("Got createdAt: 2025-01-01");
+ });
+
+ test("logged in user prop from fragment - fail", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchLoggedInUserPropQuery",
+ data: {
+ loggedInUser: {
+ id: "user-1",
+ createdAt: null,
+ },
+ },
+ graphqlErrors: [{ path: ["loggedInUser", "createdAt"] }],
+ });
+
+ t.render(test_catch("TestLoggedInUserPropFragmentData"));
+ await t.screen.findByText("Error!");
+ });
+
+ test("member prop - success", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchMemberPropQuery",
+ data: {
+ member: {
+ __typename: "User",
+ id: "user-1",
+ createdAt: date.toISOString(),
+ },
+ },
+ });
+
+ t.render(test_catch("TestMember"));
+ await t.screen.findByText("Got user id: user-1, and createdAt: 2025-01-01");
+ });
+
+ test("member prop - fail", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchMemberPropQuery",
+ data: {
+ member: null,
+ },
+ graphqlErrors: [{ path: ["member"] }],
+ });
+
+ t.render(test_catch("TestMember"));
+ await t.screen.findByText("Error!");
+ });
+
+ test("member prop - success nested", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchMemberPropNestedQuery",
+ data: {
+ member: {
+ __typename: "User",
+ id: "user-1",
+ memberOfSingular: {
+ __typename: "User",
+ id: "user-2",
+ createdAt: date.toISOString(),
+ },
+ },
+ },
+ });
+
+ t.render(test_catch("TestMemberNested"));
+ await t.screen.findByText("Got user id: user-1, and createdAt: 2025-01-01");
+ });
+
+ test("member prop - fail nested", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchMemberPropNestedQuery",
+ data: {
+ member: {
+ __typename: "User",
+ id: "user-1",
+ memberOfSingular: {
+ __typename: "User",
+ id: "user-2",
+ createdAt: null,
+ },
+ },
+ },
+ graphqlErrors: [{ path: ["member", "memberOfSingular", "createdAt"] }],
+ });
+
+ t.render(test_catch("TestMemberNested"));
+ await t.screen.findByText("Error nested!");
+ });
+
+ test("members array", async () => {
+ queryMock.mockQuery({
+ name: "TestCatchMembersPropQuery",
+ data: {
+ members: {
+ edges: [
+ {
+ __typename: "UserEdge",
+ node: {
+ __typename: "User",
+ id: "user-1",
+ createdAt: date.toISOString(),
+ },
+ },
+ {
+ __typename: "UserEdge",
+ node: {
+ __typename: "User",
+ id: "user-2",
+ createdAt: null,
+ },
+ },
+ {
+ __typename: "UserEdge",
+ node: {
+ __typename: "User",
+ id: "user-3",
+ createdAt: date.toISOString(),
+ },
+ },
+ ],
+ },
+ },
+ graphqlErrors: [{ path: ["members", "edges", 1, "node", "createdAt"] }],
+ });
+
+ t.render(test_catch("TestMembers"));
+ await t.screen.findByText(
+ "User: user-1 - 2025-01-01, Error!, User: user-3 - 2025-01-01"
+ );
+ });
+});
diff --git a/packages/rescript-relay/__tests__/Test_catch.res b/packages/rescript-relay/__tests__/Test_catch.res
new file mode 100644
index 00000000..d9ac38a2
--- /dev/null
+++ b/packages/rescript-relay/__tests__/Test_catch.res
@@ -0,0 +1,178 @@
+module QueryLoggedInUserProp = %relay(`
+ query TestCatchLoggedInUserPropQuery {
+ loggedInUser {
+ createdAt @catch
+ ...TestCatchUser_user
+ }
+ }
+`)
+
+module LoggedInUserFragment = %relay(`
+ fragment TestCatchUser_user on User @catch {
+ createdAt
+ }
+`)
+
+module TestLoggedInUserProp = {
+ @react.component
+ let make = () => {
+ let query = QueryLoggedInUserProp.use(~variables=())
+
+ switch query.loggedInUser.createdAt {
+ | Ok({value: createdAt}) =>
+
+ {React.string(
+ "Got createdAt: " ++ createdAt->Js.Date.toISOString->Js.String2.slice(~from=0, ~to_=10),
+ )}
+
+ | Error(_) => {React.string("Error!")}
+ }
+ }
+}
+
+module TestLoggedInUserPropFragmentData = {
+ @react.component
+ let make = () => {
+ let query = QueryLoggedInUserProp.use(~variables=())
+ let fragmentData = LoggedInUserFragment.use(query.loggedInUser.fragmentRefs)
+
+ switch fragmentData {
+ | Ok({value: {createdAt}}) =>
+
+ {React.string(
+ "Got createdAt: " ++ createdAt->Js.Date.toISOString->Js.String2.slice(~from=0, ~to_=10),
+ )}
+
+ | Error(_) => {React.string("Error!")}
+ }
+ }
+}
+
+module QueryMember = %relay(`
+ query TestCatchMemberPropQuery {
+ member(id: "123") @catch {
+ ... on User {
+ id
+ createdAt
+ }
+ }
+ }
+`)
+
+module TestMember = {
+ @react.component
+ let make = () => {
+ let query = QueryMember.use(~variables=())
+
+ switch query.member {
+ | Ok({value: User({id, createdAt})}) =>
+
+ {React.string(
+ "Got user id: " ++
+ id ++
+ ", and createdAt: " ++
+ createdAt->Js.Date.toISOString->Js.String2.slice(~from=0, ~to_=10),
+ )}
+
+ | Error(_) => {React.string("Error!")}
+ | _ => React.null
+ }
+ }
+}
+
+module QueryMemberNested = %relay(`
+ query TestCatchMemberPropNestedQuery {
+ member(id: "123") {
+ ... on User {
+ id
+ memberOfSingular @catch {
+ ... on User {
+ id
+ createdAt
+ }
+ }
+ }
+ }
+ }
+`)
+
+module TestMemberNested = {
+ @react.component
+ let make = () => {
+ let query = QueryMemberNested.use(~variables=())
+
+ switch query.member {
+ | Some(User({id, memberOfSingular: Ok({value: User({createdAt})})})) =>
+
+ {React.string(
+ "Got user id: " ++
+ id ++
+ ", and createdAt: " ++
+ createdAt->Js.Date.toISOString->Js.String2.slice(~from=0, ~to_=10),
+ )}
+
+ | Some(User({memberOfSingular: Error(_)})) => {React.string("Error nested!")}
+ | _ => React.null
+ }
+ }
+}
+
+module QueryMembers = %relay(`
+ query TestCatchMembersPropQuery {
+ members(groupId: "123") {
+ edges {
+ node @catch {
+ ... on User {
+ id
+ createdAt
+ }
+ }
+ }
+ }
+ }
+`)
+
+module TestMembers = {
+ @react.component
+ let make = () => {
+ let query = QueryMembers.use(~variables=())
+
+ let members =
+ query.members
+ ->Belt.Option.flatMap(v => v.edges)
+ ->Belt.Option.getWithDefault([])
+ ->Belt.Array.keepMap(x => x->Belt.Option.map(r => r.node))
+
+ members
+ ->Js.Array2.map(r =>
+ switch r {
+ | Ok({value: User({id, createdAt})}) =>
+ `User: ${id} - ${createdAt->Js.Date.toISOString->Js.String2.slice(~from=0, ~to_=10)}`
+ | _ => "Error!"
+ }
+ )
+ ->Js.Array2.joinWith(", ")
+ ->React.string
+ }
+}
+
+@live
+let test_catch = testName => {
+ let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=RelayEnv.fetchQuery)
+
+ let environment = RescriptRelay.Environment.make(
+ ~network,
+ ~store=RescriptRelay.Store.make(~source=RescriptRelay.RecordSource.make()),
+ )
+
+
+ {switch testName {
+ | "TestLoggedInUserProp" =>
+ | "TestLoggedInUserPropFragmentData" =>
+ | "TestMember" =>
+ | "TestMemberNested" =>
+ | "TestMembers" =>
+ | _ => React.null
+ }}
+
+}
diff --git a/packages/rescript-relay/__tests__/TestsUtils.res b/packages/rescript-relay/__tests__/TestsUtils.res
index 89223175..0eec8a3f 100644
--- a/packages/rescript-relay/__tests__/TestsUtils.res
+++ b/packages/rescript-relay/__tests__/TestsUtils.res
@@ -2,7 +2,9 @@ exception Malformed_date
exception Malformed_number
module Datetime = {
+ @editor.completeFrom(Js.Date)
type t = Js.Date.t
+
let parse = t =>
switch t->Js.Json.decodeString {
| None => raise(Malformed_date)
diff --git a/packages/rescript-relay/__tests__/__generated__/TestCatchLoggedInUserPropQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestCatchLoggedInUserPropQuery_graphql.res
new file mode 100644
index 00000000..9a322845
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/TestCatchLoggedInUserPropQuery_graphql.res
@@ -0,0 +1,207 @@
+/* @sourceLoc Test_catch.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type rec response_loggedInUser = {
+ createdAt: RescriptRelay.CatchResult.t,
+ fragmentRefs: RescriptRelay.fragmentRefs<[ | #TestCatchUser_user]>,
+ }
+ type response = {
+ loggedInUser: response_loggedInUser,
+ }
+ @live
+ type rawResponse = response
+ @live
+ type variables = unit
+ @live
+ type refetchVariables = unit
+ @live let makeRefetchVariables = () => ()
+}
+
+
+type queryRef
+
+module Internal = {
+ @live
+ let variablesConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let variablesConverterMap = ()
+ @live
+ let convertVariables = v => v->RescriptRelay.convertObj(
+ variablesConverter,
+ variablesConverterMap,
+ Js.undefined
+ )
+ @live
+ type wrapResponseRaw
+ @live
+ let wrapResponseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"loggedInUser_createdAt_value":{"c":"TestsUtils.Datetime"},"loggedInUser":{"f":""}}}`
+ )
+ @live
+ let wrapResponseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.serialize,
+ }
+ @live
+ let convertWrapResponse = v => v->RescriptRelay.convertObj(
+ wrapResponseConverter,
+ wrapResponseConverterMap,
+ Js.null
+ )
+ @live
+ type responseRaw
+ @live
+ let responseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"loggedInUser_createdAt_value":{"c":"TestsUtils.Datetime"},"loggedInUser":{"f":""}}}`
+ )
+ @live
+ let responseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.parse,
+ }
+ @live
+ let convertResponse = v => v->RescriptRelay.convertObj(
+ responseConverter,
+ responseConverterMap,
+ Js.undefined
+ )
+ type wrapRawResponseRaw = wrapResponseRaw
+ @live
+ let convertWrapRawResponse = convertWrapResponse
+ type rawResponseRaw = responseRaw
+ @live
+ let convertRawResponse = convertResponse
+ type rawPreloadToken<'response> = {source: Js.Nullable.t>}
+ external tokenToRaw: queryRef => rawPreloadToken = "%identity"
+}
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.queryNode
+
+
+let node: operationType = %raw(json` (function(){
+var v0 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "TestCatchLoggedInUserPropQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "loggedInUser",
+ "plural": false,
+ "selections": [
+ {
+ "kind": "CatchField",
+ "field": (v0/*: any*/),
+ "to": "RESULT"
+ },
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "TestCatchUser_user"
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "TestCatchLoggedInUserPropQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "User",
+ "kind": "LinkedField",
+ "name": "loggedInUser",
+ "plural": false,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "3f1f4cf7afa2f93882110139d958809c",
+ "id": null,
+ "metadata": {},
+ "name": "TestCatchLoggedInUserPropQuery",
+ "operationKind": "query",
+ "text": "query TestCatchLoggedInUserPropQuery {\n loggedInUser {\n createdAt\n ...TestCatchUser_user\n id\n }\n}\n\nfragment TestCatchUser_user on User {\n createdAt\n}\n"
+ }
+};
+})() `)
+
+@live let load: (
+ ~environment: RescriptRelay.Environment.t,
+ ~variables: Types.variables,
+ ~fetchPolicy: RescriptRelay.fetchPolicy=?,
+ ~fetchKey: string=?,
+ ~networkCacheConfig: RescriptRelay.cacheConfig=?,
+) => queryRef = (
+ ~environment,
+ ~variables,
+ ~fetchPolicy=?,
+ ~fetchKey=?,
+ ~networkCacheConfig=?,
+) =>
+ RescriptRelay.loadQuery(
+ environment,
+ node,
+ variables->Internal.convertVariables,
+ {
+ fetchKey,
+ fetchPolicy,
+ networkCacheConfig,
+ },
+ )
+
+@live
+let queryRefToObservable = token => {
+ let raw = token->Internal.tokenToRaw
+ raw.source->Js.Nullable.toOption
+}
+
+@live
+let queryRefToPromise = token => {
+ Js.Promise.make((~resolve, ~reject as _) => {
+ switch token->queryRefToObservable {
+ | None => resolve(Error())
+ | Some(o) =>
+ open RescriptRelay.Observable
+ let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok())))
+ }
+ })
+}
diff --git a/packages/rescript-relay/__tests__/__generated__/TestCatchMemberPropNestedQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestCatchMemberPropNestedQuery_graphql.res
new file mode 100644
index 00000000..2ae56de6
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/TestCatchMemberPropNestedQuery_graphql.res
@@ -0,0 +1,304 @@
+/* @sourceLoc Test_catch.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ @tag("__typename") type response_member_User_memberOfSingular_value =
+ | @live User(
+ {
+ @live __typename: [ | #User],
+ createdAt: TestsUtils.Datetime.t,
+ @live id: string,
+ }
+ )
+ | @live @as("__unselected") UnselectedUnionMember(string)
+
+ @tag("__typename") type response_member =
+ | @live User(
+ {
+ @live __typename: [ | #User],
+ @live id: string,
+ memberOfSingular: RescriptRelay.CatchResult.t,
+ }
+ )
+ | @live @as("__unselected") UnselectedUnionMember(string)
+
+ type response = {
+ member: option,
+ }
+ @live
+ type rawResponse = response
+ @live
+ type variables = unit
+ @live
+ type refetchVariables = unit
+ @live let makeRefetchVariables = () => ()
+}
+
+@live
+let unwrap_response_member_User_memberOfSingular_value: Types.response_member_User_memberOfSingular_value => Types.response_member_User_memberOfSingular_value = RescriptRelay_Internal.unwrapUnion(_, ["User"])
+@live
+let wrap_response_member_User_memberOfSingular_value: Types.response_member_User_memberOfSingular_value => Types.response_member_User_memberOfSingular_value = RescriptRelay_Internal.wrapUnion
+@live
+let unwrap_response_member: Types.response_member => Types.response_member = RescriptRelay_Internal.unwrapUnion(_, ["User"])
+@live
+let wrap_response_member: Types.response_member => Types.response_member = RescriptRelay_Internal.wrapUnion
+
+type queryRef
+
+module Internal = {
+ @live
+ let variablesConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let variablesConverterMap = ()
+ @live
+ let convertVariables = v => v->RescriptRelay.convertObj(
+ variablesConverter,
+ variablesConverterMap,
+ Js.undefined
+ )
+ @live
+ type wrapResponseRaw
+ @live
+ let wrapResponseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"member_User_memberOfSingular_value_User_createdAt":{"c":"TestsUtils.Datetime"},"member_User_memberOfSingular_value":{"u":"response_member_User_memberOfSingular_value"},"member":{"u":"response_member"}}}`
+ )
+ @live
+ let wrapResponseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.serialize,
+ "response_member_User_memberOfSingular_value": wrap_response_member_User_memberOfSingular_value,
+ "response_member": wrap_response_member,
+ }
+ @live
+ let convertWrapResponse = v => v->RescriptRelay.convertObj(
+ wrapResponseConverter,
+ wrapResponseConverterMap,
+ Js.null
+ )
+ @live
+ type responseRaw
+ @live
+ let responseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"member_User_memberOfSingular_value_User_createdAt":{"c":"TestsUtils.Datetime"},"member_User_memberOfSingular_value":{"u":"response_member_User_memberOfSingular_value"},"member":{"u":"response_member"}}}`
+ )
+ @live
+ let responseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.parse,
+ "response_member_User_memberOfSingular_value": unwrap_response_member_User_memberOfSingular_value,
+ "response_member": unwrap_response_member,
+ }
+ @live
+ let convertResponse = v => v->RescriptRelay.convertObj(
+ responseConverter,
+ responseConverterMap,
+ Js.undefined
+ )
+ type wrapRawResponseRaw = wrapResponseRaw
+ @live
+ let convertWrapRawResponse = convertWrapResponse
+ type rawResponseRaw = responseRaw
+ @live
+ let convertRawResponse = convertResponse
+ type rawPreloadToken<'response> = {source: Js.Nullable.t>}
+ external tokenToRaw: queryRef => rawPreloadToken = "%identity"
+}
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.queryNode
+
+
+let node: operationType = %raw(json` (function(){
+var v0 = [
+ {
+ "kind": "Literal",
+ "name": "id",
+ "value": "123"
+ }
+],
+v1 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+},
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+},
+v3 = {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+},
+v4 = {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "type": "Node",
+ "abstractKey": "__isNode"
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "TestCatchMemberPropNestedQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v0/*: any*/),
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "member",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/),
+ {
+ "kind": "CatchField",
+ "field": {
+ "alias": null,
+ "args": null,
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "memberOfSingular",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ (v3/*: any*/)
+ ],
+ "storageKey": null
+ },
+ "to": "RESULT"
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+ }
+ ],
+ "storageKey": "member(id:\"123\")"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "TestCatchMemberPropNestedQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v0/*: any*/),
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "member",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "memberOfSingular",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ (v3/*: any*/),
+ (v4/*: any*/)
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+ },
+ (v4/*: any*/)
+ ],
+ "storageKey": "member(id:\"123\")"
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "615cea126472d95d81dbe3a878b145a9",
+ "id": null,
+ "metadata": {},
+ "name": "TestCatchMemberPropNestedQuery",
+ "operationKind": "query",
+ "text": "query TestCatchMemberPropNestedQuery {\n member(id: \"123\") {\n __typename\n ... on User {\n id\n memberOfSingular {\n __typename\n ... on User {\n id\n createdAt\n }\n ... on Node {\n __isNode: __typename\n __typename\n id\n }\n }\n }\n ... on Node {\n __isNode: __typename\n __typename\n id\n }\n }\n}\n"
+ }
+};
+})() `)
+
+@live let load: (
+ ~environment: RescriptRelay.Environment.t,
+ ~variables: Types.variables,
+ ~fetchPolicy: RescriptRelay.fetchPolicy=?,
+ ~fetchKey: string=?,
+ ~networkCacheConfig: RescriptRelay.cacheConfig=?,
+) => queryRef = (
+ ~environment,
+ ~variables,
+ ~fetchPolicy=?,
+ ~fetchKey=?,
+ ~networkCacheConfig=?,
+) =>
+ RescriptRelay.loadQuery(
+ environment,
+ node,
+ variables->Internal.convertVariables,
+ {
+ fetchKey,
+ fetchPolicy,
+ networkCacheConfig,
+ },
+ )
+
+@live
+let queryRefToObservable = token => {
+ let raw = token->Internal.tokenToRaw
+ raw.source->Js.Nullable.toOption
+}
+
+@live
+let queryRefToPromise = token => {
+ Js.Promise.make((~resolve, ~reject as _) => {
+ switch token->queryRefToObservable {
+ | None => resolve(Error())
+ | Some(o) =>
+ open RescriptRelay.Observable
+ let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok())))
+ }
+ })
+}
diff --git a/packages/rescript-relay/__tests__/__generated__/TestCatchMemberPropQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestCatchMemberPropQuery_graphql.res
new file mode 100644
index 00000000..b7b79f68
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/TestCatchMemberPropQuery_graphql.res
@@ -0,0 +1,246 @@
+/* @sourceLoc Test_catch.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ @tag("__typename") type response_member_value =
+ | @live User(
+ {
+ @live __typename: [ | #User],
+ createdAt: TestsUtils.Datetime.t,
+ @live id: string,
+ }
+ )
+ | @live @as("__unselected") UnselectedUnionMember(string)
+
+ type response = {
+ member: RescriptRelay.CatchResult.t,
+ }
+ @live
+ type rawResponse = response
+ @live
+ type variables = unit
+ @live
+ type refetchVariables = unit
+ @live let makeRefetchVariables = () => ()
+}
+
+@live
+let unwrap_response_member_value: Types.response_member_value => Types.response_member_value = RescriptRelay_Internal.unwrapUnion(_, ["User"])
+@live
+let wrap_response_member_value: Types.response_member_value => Types.response_member_value = RescriptRelay_Internal.wrapUnion
+
+type queryRef
+
+module Internal = {
+ @live
+ let variablesConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let variablesConverterMap = ()
+ @live
+ let convertVariables = v => v->RescriptRelay.convertObj(
+ variablesConverter,
+ variablesConverterMap,
+ Js.undefined
+ )
+ @live
+ type wrapResponseRaw
+ @live
+ let wrapResponseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"member_value_User_createdAt":{"c":"TestsUtils.Datetime"},"member_value":{"u":"response_member_value"}}}`
+ )
+ @live
+ let wrapResponseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.serialize,
+ "response_member_value": wrap_response_member_value,
+ }
+ @live
+ let convertWrapResponse = v => v->RescriptRelay.convertObj(
+ wrapResponseConverter,
+ wrapResponseConverterMap,
+ Js.null
+ )
+ @live
+ type responseRaw
+ @live
+ let responseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"member_value_User_createdAt":{"c":"TestsUtils.Datetime"},"member_value":{"u":"response_member_value"}}}`
+ )
+ @live
+ let responseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.parse,
+ "response_member_value": unwrap_response_member_value,
+ }
+ @live
+ let convertResponse = v => v->RescriptRelay.convertObj(
+ responseConverter,
+ responseConverterMap,
+ Js.undefined
+ )
+ type wrapRawResponseRaw = wrapResponseRaw
+ @live
+ let convertWrapRawResponse = convertWrapResponse
+ type rawResponseRaw = responseRaw
+ @live
+ let convertRawResponse = convertResponse
+ type rawPreloadToken<'response> = {source: Js.Nullable.t>}
+ external tokenToRaw: queryRef => rawPreloadToken = "%identity"
+}
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.queryNode
+
+
+let node: operationType = %raw(json` (function(){
+var v0 = [
+ {
+ "kind": "Literal",
+ "name": "id",
+ "value": "123"
+ }
+],
+v1 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+},
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+},
+v3 = {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "TestCatchMemberPropQuery",
+ "selections": [
+ {
+ "kind": "CatchField",
+ "field": {
+ "alias": null,
+ "args": (v0/*: any*/),
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "member",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ (v3/*: any*/)
+ ],
+ "storageKey": "member(id:\"123\")"
+ },
+ "to": "RESULT"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "TestCatchMemberPropQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v0/*: any*/),
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "member",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ (v3/*: any*/),
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "type": "Node",
+ "abstractKey": "__isNode"
+ }
+ ],
+ "storageKey": "member(id:\"123\")"
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "ac6dc290592743f843d1daec5045034c",
+ "id": null,
+ "metadata": {},
+ "name": "TestCatchMemberPropQuery",
+ "operationKind": "query",
+ "text": "query TestCatchMemberPropQuery {\n member(id: \"123\") {\n __typename\n ... on User {\n id\n createdAt\n }\n ... on Node {\n __isNode: __typename\n __typename\n id\n }\n }\n}\n"
+ }
+};
+})() `)
+
+@live let load: (
+ ~environment: RescriptRelay.Environment.t,
+ ~variables: Types.variables,
+ ~fetchPolicy: RescriptRelay.fetchPolicy=?,
+ ~fetchKey: string=?,
+ ~networkCacheConfig: RescriptRelay.cacheConfig=?,
+) => queryRef = (
+ ~environment,
+ ~variables,
+ ~fetchPolicy=?,
+ ~fetchKey=?,
+ ~networkCacheConfig=?,
+) =>
+ RescriptRelay.loadQuery(
+ environment,
+ node,
+ variables->Internal.convertVariables,
+ {
+ fetchKey,
+ fetchPolicy,
+ networkCacheConfig,
+ },
+ )
+
+@live
+let queryRefToObservable = token => {
+ let raw = token->Internal.tokenToRaw
+ raw.source->Js.Nullable.toOption
+}
+
+@live
+let queryRefToPromise = token => {
+ Js.Promise.make((~resolve, ~reject as _) => {
+ switch token->queryRefToObservable {
+ | None => resolve(Error())
+ | Some(o) =>
+ open RescriptRelay.Observable
+ let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok())))
+ }
+ })
+}
diff --git a/packages/rescript-relay/__tests__/__generated__/TestCatchMembersPropQuery_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestCatchMembersPropQuery_graphql.res
new file mode 100644
index 00000000..bf2afc05
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/TestCatchMembersPropQuery_graphql.res
@@ -0,0 +1,296 @@
+/* @sourceLoc Test_catch.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ @tag("__typename") type response_members_edges_node_value =
+ | @live User(
+ {
+ @live __typename: [ | #User],
+ createdAt: TestsUtils.Datetime.t,
+ @live id: string,
+ }
+ )
+ | @live @as("__unselected") UnselectedUnionMember(string)
+
+ type rec response_members_edges = {
+ node: RescriptRelay.CatchResult.t,
+ }
+ and response_members = {
+ edges: option>>,
+ }
+ type response = {
+ members: option,
+ }
+ @live
+ type rawResponse = response
+ @live
+ type variables = unit
+ @live
+ type refetchVariables = unit
+ @live let makeRefetchVariables = () => ()
+}
+
+@live
+let unwrap_response_members_edges_node_value: Types.response_members_edges_node_value => Types.response_members_edges_node_value = RescriptRelay_Internal.unwrapUnion(_, ["User"])
+@live
+let wrap_response_members_edges_node_value: Types.response_members_edges_node_value => Types.response_members_edges_node_value = RescriptRelay_Internal.wrapUnion
+
+type queryRef
+
+module Internal = {
+ @live
+ let variablesConverter: Js.Dict.t>> = %raw(
+ json`{}`
+ )
+ @live
+ let variablesConverterMap = ()
+ @live
+ let convertVariables = v => v->RescriptRelay.convertObj(
+ variablesConverter,
+ variablesConverterMap,
+ Js.undefined
+ )
+ @live
+ type wrapResponseRaw
+ @live
+ let wrapResponseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"members_edges_node_value_User_createdAt":{"c":"TestsUtils.Datetime"},"members_edges_node_value":{"u":"response_members_edges_node_value"}}}`
+ )
+ @live
+ let wrapResponseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.serialize,
+ "response_members_edges_node_value": wrap_response_members_edges_node_value,
+ }
+ @live
+ let convertWrapResponse = v => v->RescriptRelay.convertObj(
+ wrapResponseConverter,
+ wrapResponseConverterMap,
+ Js.null
+ )
+ @live
+ type responseRaw
+ @live
+ let responseConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"members_edges_node_value_User_createdAt":{"c":"TestsUtils.Datetime"},"members_edges_node_value":{"u":"response_members_edges_node_value"}}}`
+ )
+ @live
+ let responseConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.parse,
+ "response_members_edges_node_value": unwrap_response_members_edges_node_value,
+ }
+ @live
+ let convertResponse = v => v->RescriptRelay.convertObj(
+ responseConverter,
+ responseConverterMap,
+ Js.undefined
+ )
+ type wrapRawResponseRaw = wrapResponseRaw
+ @live
+ let convertWrapRawResponse = convertWrapResponse
+ type rawResponseRaw = responseRaw
+ @live
+ let convertRawResponse = convertResponse
+ type rawPreloadToken<'response> = {source: Js.Nullable.t>}
+ external tokenToRaw: queryRef => rawPreloadToken = "%identity"
+}
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.queryNode
+
+
+let node: operationType = %raw(json` (function(){
+var v0 = [
+ {
+ "kind": "Literal",
+ "name": "groupId",
+ "value": "123"
+ }
+],
+v1 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "__typename",
+ "storageKey": null
+},
+v2 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+},
+v3 = {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+};
+return {
+ "fragment": {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "TestCatchMembersPropQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v0/*: any*/),
+ "concreteType": "MemberConnection",
+ "kind": "LinkedField",
+ "name": "members",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "MemberEdge",
+ "kind": "LinkedField",
+ "name": "edges",
+ "plural": true,
+ "selections": [
+ {
+ "kind": "CatchField",
+ "field": {
+ "alias": null,
+ "args": null,
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "node",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ (v3/*: any*/)
+ ],
+ "storageKey": null
+ },
+ "to": "RESULT"
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": "members(groupId:\"123\")"
+ }
+ ],
+ "type": "Query",
+ "abstractKey": null
+ },
+ "kind": "Request",
+ "operation": {
+ "argumentDefinitions": [],
+ "kind": "Operation",
+ "name": "TestCatchMembersPropQuery",
+ "selections": [
+ {
+ "alias": null,
+ "args": (v0/*: any*/),
+ "concreteType": "MemberConnection",
+ "kind": "LinkedField",
+ "name": "members",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "MemberEdge",
+ "kind": "LinkedField",
+ "name": "edges",
+ "plural": true,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": null,
+ "kind": "LinkedField",
+ "name": "node",
+ "plural": false,
+ "selections": [
+ (v1/*: any*/),
+ (v3/*: any*/),
+ {
+ "kind": "InlineFragment",
+ "selections": [
+ (v2/*: any*/)
+ ],
+ "type": "Node",
+ "abstractKey": "__isNode"
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "storageKey": "members(groupId:\"123\")"
+ }
+ ]
+ },
+ "params": {
+ "cacheID": "98d239a936e0ac54023a142c978b3102",
+ "id": null,
+ "metadata": {},
+ "name": "TestCatchMembersPropQuery",
+ "operationKind": "query",
+ "text": "query TestCatchMembersPropQuery {\n members(groupId: \"123\") {\n edges {\n node {\n __typename\n ... on User {\n id\n createdAt\n }\n ... on Node {\n __isNode: __typename\n __typename\n id\n }\n }\n }\n }\n}\n"
+ }
+};
+})() `)
+
+@live let load: (
+ ~environment: RescriptRelay.Environment.t,
+ ~variables: Types.variables,
+ ~fetchPolicy: RescriptRelay.fetchPolicy=?,
+ ~fetchKey: string=?,
+ ~networkCacheConfig: RescriptRelay.cacheConfig=?,
+) => queryRef = (
+ ~environment,
+ ~variables,
+ ~fetchPolicy=?,
+ ~fetchKey=?,
+ ~networkCacheConfig=?,
+) =>
+ RescriptRelay.loadQuery(
+ environment,
+ node,
+ variables->Internal.convertVariables,
+ {
+ fetchKey,
+ fetchPolicy,
+ networkCacheConfig,
+ },
+ )
+
+@live
+let queryRefToObservable = token => {
+ let raw = token->Internal.tokenToRaw
+ raw.source->Js.Nullable.toOption
+}
+
+@live
+let queryRefToPromise = token => {
+ Js.Promise.make((~resolve, ~reject as _) => {
+ switch token->queryRefToObservable {
+ | None => resolve(Error())
+ | Some(o) =>
+ open RescriptRelay.Observable
+ let _: subscription = o->subscribe(makeObserver(~complete=() => resolve(Ok())))
+ }
+ })
+}
diff --git a/packages/rescript-relay/__tests__/__generated__/TestCatchUser_user_graphql.res b/packages/rescript-relay/__tests__/__generated__/TestCatchUser_user_graphql.res
new file mode 100644
index 00000000..2e0c411a
--- /dev/null
+++ b/packages/rescript-relay/__tests__/__generated__/TestCatchUser_user_graphql.res
@@ -0,0 +1,65 @@
+/* @sourceLoc Test_catch.res */
+/* @generated */
+%%raw("/* @generated */")
+module Types = {
+ @@warning("-30")
+
+ type fragment_t = {
+ createdAt: TestsUtils.Datetime.t,
+ }
+ type fragment = RescriptRelay.CatchResult.t
+}
+
+module Internal = {
+ @live
+ type fragmentRaw
+ @live
+ let fragmentConverter: Js.Dict.t>> = %raw(
+ json`{"__root":{"value_createdAt":{"c":"TestsUtils.Datetime"}}}`
+ )
+ @live
+ let fragmentConverterMap = {
+ "TestsUtils.Datetime": TestsUtils.Datetime.parse,
+ }
+ @live
+ let convertFragment = v => v->RescriptRelay.convertObj(
+ fragmentConverter,
+ fragmentConverterMap,
+ Js.undefined
+ )
+}
+
+type t
+type fragmentRef
+external getFragmentRef:
+ RescriptRelay.fragmentRefs<[> | #TestCatchUser_user]> => fragmentRef = "%identity"
+
+module Utils = {
+ @@warning("-33")
+ open Types
+}
+
+type relayOperationNode
+type operationType = RescriptRelay.fragmentNode
+
+
+let node: operationType = %raw(json` {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": {
+ "catchTo": "RESULT"
+ },
+ "name": "TestCatchUser_user",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ }
+ ],
+ "type": "User",
+ "abstractKey": null
+} `)
+
diff --git a/packages/rescript-relay/src/RescriptRelay.res b/packages/rescript-relay/src/RescriptRelay.res
index 64302d5c..f19fd059 100644
--- a/packages/rescript-relay/src/RescriptRelay.res
+++ b/packages/rescript-relay/src/RescriptRelay.res
@@ -17,6 +17,25 @@ type dataIdObject = {id: dataId}
type recordSourceRecords = Js.Json.t
type uploadables
+module CatchResult = {
+ type catchError = Js.Json.t
+
+ @tag("ok")
+ type t<'value> = | @as(true) Ok({value: 'value}) | @as(false) Error({errors: array})
+
+ let toOption = (t: t<'value>) =>
+ switch t {
+ | Ok({value}) => Some(value)
+ | Error(_) => None
+ }
+
+ let toResult = (t: t<'value>): result<'value, array> =>
+ switch t {
+ | Ok({value}) => Ok(value)
+ | Error({errors}) => Error(errors)
+ }
+}
+
module SuspenseSentinel = {
type t
diff --git a/packages/rescript-relay/src/RescriptRelay.resi b/packages/rescript-relay/src/RescriptRelay.resi
index 405d250a..a6659985 100644
--- a/packages/rescript-relay/src/RescriptRelay.resi
+++ b/packages/rescript-relay/src/RescriptRelay.resi
@@ -41,6 +41,24 @@ type dataId
type dataIdObject = {id: dataId}
+/** A module for results originating from the @catch directive. */
+module CatchResult: {
+ /** The shape of an error caught via @catch. */
+ type catchError = Js.Json.t
+
+ /** The result type for @catch. */
+ @tag("ok")
+ type t<'value> =
+ | @as(true) Ok({value: 'value})
+ | @as(false) Error({errors: array})
+
+ /** Convert a @catch result to option. */
+ let toOption: t<'value> => option<'value>
+
+ /** Convert a @catch result to result. */
+ let toResult: t<'value> => result<'value, array>
+}
+
module SuspenseSentinel: {
type t