From 8e538646a4ac927a1a9195b62a3b659f5b2311e9 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Tue, 15 Aug 2023 11:32:01 +0930 Subject: [PATCH 1/3] Add utility function for incremental response. Add utility function to convert GraphQL spec incremental response for @defer/@stream fragments so that they work with the expected shape from Relay. --- .../src/RescriptRelay_NetworkUtils.res | 42 +++++++++++++++++++ .../src/RescriptRelay_NetworkUtils.resi | 31 ++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 packages/rescript-relay/src/RescriptRelay_NetworkUtils.res create mode 100644 packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res new file mode 100644 index 00000000..2b57ea6b --- /dev/null +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res @@ -0,0 +1,42 @@ + +module GraphQLIncrementalResponse = { + type data = {.} + type t<'a> = {incremental: array<{..} as 'a>, hasNext: bool} + + let mapWithDefault: ( + Js.Json.t, + 'a => array<'d>, + 'c => array<'d>, + ) => array = %raw(`function(response, f, b) { + if (response.incremental) + return f(response); + else return b(response); + +}`) + external fromJson: Js.Json.t => t<'a> = "%identity" +} + +module RelayDeferResponse = { + type extension = {is_final: bool} + type t<'a> = {.."hasNext": bool, "extensions": extension} as 'a + + let fromIncrementalResponse: GraphQLIncrementalResponse.t<'a> => array> = ({ + incremental, + hasNext, + }) => { + incremental->Array.mapWithIndex((data, i) => { + let hasNext = i === incremental->Array.length - 1 ? hasNext : true + + Object.assign(data, {"hasNext": hasNext, "extensions": {"is_final": !hasNext}}) + }) + } + external toJson: t<'a> => Js.Json.t = "%identity" +} +let adaptIncrementalResponseToRelay = part => + part->GraphQLIncrementalResponse.mapWithDefault( + json => { + open RelayDeferResponse + json->GraphQLIncrementalResponse.fromJson->fromIncrementalResponse->Array.map(toJson) + }, + part => [part], + ) diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi new file mode 100644 index 00000000..0c0a7a2e --- /dev/null +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi @@ -0,0 +1,31 @@ +module GraphQLIncrementalResponse: { + type data = {.} + type t<'a> = {incremental: array<'a>, hasNext: bool} + constraint 'a = {..} + let mapWithDefault: (. + Js.Json.t, + (. 'a) => array<'d>, + (. 'c) => array<'d>, +) => array + let fromJson: (. Js.Json.t) => t<{..}> +} + +module RelayDeferResponse: { + type extension = {is_final: bool} + type t<'a> = 'a + constraint 'a = {.. + "extensions": extension, + "hasNext": bool, + } + let fromIncrementalResponse: (. + GraphQLIncrementalResponse.t< + {.."extensions": extension, "hasNext": bool}, + >, +) => array> + let toJson: (. + t<{.."extensions": extension, "hasNext": bool}>, +) => Js.Json.t +} + +let adaptIncrementalResponseToRelay: +(. Js.Json.t) => array From 7b7460d08693523bccaaedf342294242548ce341 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Mon, 21 Aug 2023 17:49:51 +1000 Subject: [PATCH 2/3] Refactor Defer response helper functions. Refactor Defer response converter so that the unsafe magic JS external function is just the object merge. Also implement a type-safe converter using object types, though it may not be necessary in the future as this problem should be resolved if Relay starts conforming to spec. --- .../src/RescriptRelay_NetworkUtils.res | 127 ++++++++++++++---- .../src/RescriptRelay_NetworkUtils.resi | 57 ++++---- 2 files changed, 137 insertions(+), 47 deletions(-) diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res index 2b57ea6b..c6855114 100644 --- a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res @@ -1,26 +1,95 @@ +let unsafeMergeJson: (Js.Json.t, Js.Json.t) => Js.Json.t = %raw("function (a, b) { + return { ...a, ...b}; +}") + module GraphQLIncrementalResponse = { + type t<'a> = {incremental: array<'a>, hasNext: bool} +} + +module GraphQLResponse = { type data = {.} - type t<'a> = {incremental: array<{..} as 'a>, hasNext: bool} - - let mapWithDefault: ( - Js.Json.t, - 'a => array<'d>, - 'c => array<'d>, - ) => array = %raw(`function(response, f, b) { - if (response.incremental) - return f(response); - else return b(response); - -}`) - external fromJson: Js.Json.t => t<'a> = "%identity" + type t<'a> = Incremental(GraphQLIncrementalResponse.t<'a>) | Response('a) + + let mapIncrementalWithDefault: ( + t<'a>, + GraphQLIncrementalResponse.t<'a> => array<'b>, + 'a => array<'b>, + ) => array<'b> = (t, withIncremental, default) => { + switch t { + | Incremental(incremental) => withIncremental(incremental) + | Response(json) => default(json) + } + } + let fromIncremental = data => Incremental(data) + let makeResponse = data => Response(data) + + // Use parser to parse fully type-safe response + let parse: type a. (Js.Json.t, Js.Json.t => option) => option> = (json, parseFn) => + switch json->Js.Json.decodeObject { + | Some(dict) => + switch dict->Js.Dict.get("incremental") { + | Some(data) => + switch data->Js.Json.decodeArray { + | Some(arrayData) => + Some( + Incremental({ + incremental: arrayData->Array.map(parseFn)->Array.filterMap(x => x), + hasNext: dict + ->Js.Dict.get("hasNext") + ->Option.mapWithDefault(false, v => + v->Js.Json.decodeBoolean->Option.mapWithDefault(false, v => v) + ), + }), + ) + | None => { + + let data = parseFn(json) + switch data { + | Some(data) => Some(Response(data)) + | None => None + } + } + } + | None => { + let data = parseFn(json) + switch data { + | Some(data) => Some(Response(data)) + | None => None + } + } + } + | None => None + } + + // Partially parse response + let fromJson: Js.Json.t => t<'a> = json => + switch json->Js.Json.decodeObject { + | Some(dict) => + switch dict->Js.Dict.get("incremental") { + | Some(data) => + switch data->Js.Json.decodeArray { + | Some(arrayData) => + Incremental({ + incremental: arrayData, + hasNext: dict + ->Js.Dict.get("hasNext") + ->Option.mapWithDefault(false, v => + v->Js.Json.decodeBoolean->Option.mapWithDefault(false, v => v) + ), + }) + | None => Response(json) + } + | None => Response(json) + } + | None => Response(json) + } } module RelayDeferResponse = { - type extension = {is_final: bool} - type t<'a> = {.."hasNext": bool, "extensions": extension} as 'a + type t<'a> = array<'a> - let fromIncrementalResponse: GraphQLIncrementalResponse.t<'a> => array> = ({ + let fromIncrementalResponse: GraphQLIncrementalResponse.t<{..} as 'a> => t<{..} as 'a> = ({ incremental, hasNext, }) => { @@ -30,13 +99,25 @@ module RelayDeferResponse = { Object.assign(data, {"hasNext": hasNext, "extensions": {"is_final": !hasNext}}) }) } - external toJson: t<'a> => Js.Json.t = "%identity" + + external toJson: 'a => Js.Json.t = "%identity" + + let fromJsonIncrementalResponse: GraphQLIncrementalResponse.t => array = ({ + incremental, + hasNext, + }) => { + incremental->Array.mapWithIndex((data, i) => { + let hasNext = i === incremental->Array.length - 1 ? hasNext : true + + unsafeMergeJson(data, {"hasNext": hasNext, "extensions": {"is_final": !hasNext}}->toJson) + }) + } } -let adaptIncrementalResponseToRelay = part => - part->GraphQLIncrementalResponse.mapWithDefault( - json => { - open RelayDeferResponse - json->GraphQLIncrementalResponse.fromJson->fromIncrementalResponse->Array.map(toJson) - }, + +let adaptJsonIncrementalResponseToRelay: Js.Json.t => array = part => + part + ->GraphQLResponse.fromJson + ->GraphQLResponse.mapIncrementalWithDefault( + RelayDeferResponse.fromJsonIncrementalResponse, part => [part], ) diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi index 0c0a7a2e..349d02ee 100644 --- a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi @@ -1,31 +1,40 @@ +let unsafeMergeJson: (. Js.Json.t, Js.Json.t) => Js.Json.t + module GraphQLIncrementalResponse: { - type data = {.} type t<'a> = {incremental: array<'a>, hasNext: bool} - constraint 'a = {..} - let mapWithDefault: (. - Js.Json.t, - (. 'a) => array<'d>, - (. 'c) => array<'d>, -) => array - let fromJson: (. Js.Json.t) => t<{..}> +} + +module GraphQLResponse: { + type data = {.} + type t<'a> = + | Incremental(GraphQLIncrementalResponse.t<'a>) + | Response('a) + let mapIncrementalWithDefault: (. + t<'a>, + (. GraphQLIncrementalResponse.t<'a>) => array<'b>, + (. 'a) => array<'b>, +) => array<'b> + let fromIncremental: (. GraphQLIncrementalResponse.t<'a>) => t<'a> + let makeResponse: (. 'a) => t<'a> + let parse: 'a. (. Js.Json.t, (. Js.Json.t) => option<'a>) => option< + t<'a>, +> + let fromJson: (. Js.Json.t) => t } module RelayDeferResponse: { - type extension = {is_final: bool} - type t<'a> = 'a - constraint 'a = {.. - "extensions": extension, - "hasNext": bool, - } - let fromIncrementalResponse: (. - GraphQLIncrementalResponse.t< - {.."extensions": extension, "hasNext": bool}, - >, -) => array> - let toJson: (. - t<{.."extensions": extension, "hasNext": bool}>, -) => Js.Json.t + type t<'a> = array<'a> + // Type safe conversion from a GraphQL spec response + let fromIncrementalResponse: (. GraphQLIncrementalResponse.t<{..}>) => t<{..}> + + let toJson: (. 'a) => Js.Json.t + + // Not type safe conversion due to use of Json.t and object merging + let fromJsonIncrementalResponse: (. + GraphQLIncrementalResponse.t, +) => array } -let adaptIncrementalResponseToRelay: -(. Js.Json.t) => array +// Not type safe conversion of GraphQL spec defer response to Relay-compatible +// version +let adaptJsonIncrementalResponseToRelay: (. Js.Json.t) => array From 642a553e433ef1421558171ca0708f225d4487d3 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Mon, 21 Aug 2023 23:00:38 +1000 Subject: [PATCH 3/3] Fix type errors. Fix type errors for the interface file. Reimplement to avoid use of Array.filterMap which is included in Rescript Core. --- .../src/RescriptRelay_NetworkUtils.res | 86 +++++++++++-------- .../src/RescriptRelay_NetworkUtils.resi | 4 +- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res index c6855114..a4e295a9 100644 --- a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res @@ -1,12 +1,26 @@ -let unsafeMergeJson: (Js.Json.t, Js.Json.t) => Js.Json.t = %raw("function (a, b) { - return { ...a, ...b}; -}") - +external unsafeMergeJson: (@as(json`{}`) _, Js.Json.t, Js.Json.t) => Js.Json.t = "Object.assign" module GraphQLIncrementalResponse = { type t<'a> = {incremental: array<'a>, hasNext: bool} } +module OptionArray = { + let sequence: array> => option> = xs => { + let results = xs->Array.reduce([], (acc, next) => { + switch next { + | Some(val) => acc->Array.concat([val]) + | None => acc + } + }) + + if results->Array.length < xs->Array.length { + None + } else { + Some(results) + } + } +} + module GraphQLResponse = { type data = {.} type t<'a> = Incremental(GraphQLIncrementalResponse.t<'a>) | Response('a) @@ -25,42 +39,46 @@ module GraphQLResponse = { let makeResponse = data => Response(data) // Use parser to parse fully type-safe response - let parse: type a. (Js.Json.t, Js.Json.t => option) => option> = (json, parseFn) => - switch json->Js.Json.decodeObject { - | Some(dict) => - switch dict->Js.Dict.get("incremental") { - | Some(data) => - switch data->Js.Json.decodeArray { - | Some(arrayData) => - Some( - Incremental({ - incremental: arrayData->Array.map(parseFn)->Array.filterMap(x => x), - hasNext: dict - ->Js.Dict.get("hasNext") - ->Option.mapWithDefault(false, v => - v->Js.Json.decodeBoolean->Option.mapWithDefault(false, v => v) - ), - }), - ) - | None => { - - let data = parseFn(json) - switch data { - | Some(data) => Some(Response(data)) - | None => None + let parse: + type a. (Js.Json.t, Js.Json.t => option) => option> = + (json, parseFn) => + switch json->Js.Json.decodeObject { + | Some(dict) => + switch dict->Js.Dict.get("incremental") { + | Some(data) => + switch data->Js.Json.decodeArray { + | Some(arrayData) => + arrayData + ->Array.map(parseFn) + ->OptionArray.sequence + ->Option.flatMap(data => Some( + Incremental({ + incremental: data, + hasNext: dict + ->Js.Dict.get("hasNext") + ->Option.mapWithDefault(false, v => + v->Js.Json.decodeBoolean->Option.mapWithDefault(false, v => v) + ), + }), + )) + | None => { + let data = parseFn(json) + switch data { + | Some(data) => Some(Response(data)) + | None => None + } + } } - } - } - | None => { - let data = parseFn(json) - switch data { + | None => { + let data = parseFn(json) + switch data { | Some(data) => Some(Response(data)) | None => None + } } } + | None => None } - | None => None - } // Partially parse response let fromJson: Js.Json.t => t<'a> = json => diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi index 349d02ee..fcdebf0f 100644 --- a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi @@ -16,7 +16,7 @@ module GraphQLResponse: { ) => array<'b> let fromIncremental: (. GraphQLIncrementalResponse.t<'a>) => t<'a> let makeResponse: (. 'a) => t<'a> - let parse: 'a. (. Js.Json.t, (. Js.Json.t) => option<'a>) => option< + let parse: (. Js.Json.t, (. Js.Json.t) => option<'a>) => option< t<'a>, > let fromJson: (. Js.Json.t) => t @@ -25,7 +25,7 @@ module GraphQLResponse: { module RelayDeferResponse: { type t<'a> = array<'a> // Type safe conversion from a GraphQL spec response - let fromIncrementalResponse: (. GraphQLIncrementalResponse.t<{..}>) => t<{..}> + let fromIncrementalResponse: (. GraphQLIncrementalResponse.t<{..} as 'a>) => t<{..} as 'a> let toJson: (. 'a) => Js.Json.t