diff --git a/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res new file mode 100644 index 00000000..a4e295a9 --- /dev/null +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.res @@ -0,0 +1,141 @@ +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) + + 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) => + 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 { + | 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 t<'a> = array<'a> + + let fromIncrementalResponse: GraphQLIncrementalResponse.t<{..} as 'a> => t<{..} as 'a> = ({ + 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: '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 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 new file mode 100644 index 00000000..fcdebf0f --- /dev/null +++ b/packages/rescript-relay/src/RescriptRelay_NetworkUtils.resi @@ -0,0 +1,40 @@ +let unsafeMergeJson: (. Js.Json.t, Js.Json.t) => Js.Json.t + +module GraphQLIncrementalResponse: { + type t<'a> = {incremental: array<'a>, hasNext: bool} +} + +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: (. Js.Json.t, (. Js.Json.t) => option<'a>) => option< + t<'a>, +> + let fromJson: (. Js.Json.t) => t +} + +module RelayDeferResponse: { + type t<'a> = array<'a> + // Type safe conversion from a GraphQL spec response + let fromIncrementalResponse: (. GraphQLIncrementalResponse.t<{..} as 'a>) => t<{..} as 'a> + + let toJson: (. 'a) => Js.Json.t + + // Not type safe conversion due to use of Json.t and object merging + let fromJsonIncrementalResponse: (. + GraphQLIncrementalResponse.t, +) => array +} + +// Not type safe conversion of GraphQL spec defer response to Relay-compatible +// version +let adaptJsonIncrementalResponseToRelay: (. Js.Json.t) => array