Last active
April 27, 2020 08:45
-
-
Save kgoggin/0d712c7ea878200cb42ad77143844399 to your computer and use it in GitHub Desktop.
Custom react-apollo ReasonML bindings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open ApolloTypes; | |
type js_read_query_options = { | |
. | |
"query": queryString, | |
"variables": Js.undefined(Js.Json.t), | |
}; | |
type js_write_query_options = { | |
. | |
"query": queryString, | |
"data": Js.Json.t, | |
}; | |
[@bs.obj] | |
external makeJSReadQueryOptions: | |
(~query: queryString, ~variables: Js.Json.t=?, unit) => _ = | |
""; | |
type js_cache = { | |
. | |
"readQuery": [@bs.meth] (js_read_query_options => Js.Json.t), | |
"writeQuery": [@bs.meth] (js_write_query_options => unit), | |
}; | |
type readQuery = | |
(~query: ApolloTypes.queryString, ~variables: Js.Json.t=?, unit) => Js.Json.t; | |
type writeQuery = | |
(~query: ApolloTypes.queryString, ~data: Js.Json.t, unit) => unit; | |
type cache = { | |
readQuery, | |
writeQuery, | |
}; | |
type updateFunc('response) = (cache, 'response) => unit; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[@bs.deriving jsConverter] | |
type jsCode = [ | |
| [@bs.as "UNIQUE_CONSTRAINT"] `UniqueConstraint | |
| [@bs.as "UNAUTHENTICATED"] `Unauthenticated | |
| [@bs.as "BAD_USER_INPUT"] `BadUserInput | |
| [@bs.as "NOT_FOUND"] `NotFound | |
]; | |
[@bs.deriving abstract] | |
type jsException_ = { | |
[@bs.optional] | |
invalidArguments: array((string, string)), | |
}; | |
[@bs.deriving abstract] | |
type jsExtensions = { | |
code: string, | |
[@bs.as "exception"] | |
exception_: jsException_, | |
}; | |
[@bs.deriving abstract] | |
type jsGraphQLError = {extensions: jsExtensions}; | |
[@bs.deriving abstract] | |
type jsNetworkError = {message: string}; | |
[@bs.deriving abstract] | |
type jsError = { | |
graphQLErrors: array(jsGraphQLError), | |
networkError: Js.Nullable.t(jsNetworkError), | |
}; | |
external toApolloError: Js.Promise.error => jsError = "%identity"; | |
type graphQLError = | |
| Unauthenticated | |
| UniqueConstraint | |
| BadUserInput(list((string, string))) | |
| NotFound | |
| Unknown; | |
type t = | |
| Network(string) | |
| GraphQL(graphQLError, list(graphQLError)); | |
let mapError: jsError => t = | |
jsErr => | |
switch (jsErr->networkErrorGet->Js.Nullable.toOption) { | |
| Some(ne) => Network(ne->messageGet) | |
| None => | |
let gqlErrors = | |
jsErr->graphQLErrorsGet | |
|> Array.map(gqle => | |
switch (gqle->extensionsGet->codeGet->jsCodeFromJs) { | |
| Some(`BadUserInput) => | |
let invalidArgs = | |
switch ( | |
gqle->extensionsGet->exception_Get->invalidArgumentsGet | |
) { | |
| None => [] | |
| Some(args) => args |> Array.to_list | |
}; | |
BadUserInput(invalidArgs); | |
| Some(`Unauthenticated) => Unauthenticated | |
| Some(`UniqueConstraint) => UniqueConstraint | |
| Some(`NotFound) => NotFound | |
| None => Unknown | |
} | |
) | |
|> Array.to_list; | |
GraphQL(gqlErrors->List.hd, gqlErrors); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* An abstract type to describe a query string object. | |
*/ | |
type queryString; | |
/** | |
* The signature of the `graphql-tag/gql` function that transforms a GraphQL | |
* query string to the standard GraphQL AST. | |
* https://github.com/apollographql/graphql-tag | |
*/ | |
type gql = (. string) => queryString; | |
[@bs.module] external gql: gql = "graphql-tag"; | |
/** | |
* An abstract type to describe an Apollo Link object. | |
*/ | |
type apolloLink; | |
/** | |
* An abstract type to describe an Apollo Cache object. | |
*/ | |
type apolloCache; | |
type networkError = {. "statusCode": int}; | |
module NetworkState = { | |
type t = | |
| Loading | |
| SetVariables | |
| FetchMore | |
| Refetch | |
| Poll | |
| Ready | |
| Error; | |
let to_int = | |
fun | |
| Loading => 1 | |
| SetVariables => 2 | |
| FetchMore => 3 | |
| Refetch => 4 | |
| Poll => 6 | |
| Ready => 7 | |
| Error => 8; | |
}; | |
type apolloError; | |
/* TODO: define missing keys */ | |
type apolloLinkErrorResponse = {. "networkError": option(networkError)}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open ApolloTypes; | |
open ApolloCache; | |
open Belt; | |
[@bs.module "react-apollo"] | |
external reactApolloMutation: ReasonReact.reactClass = "Mutation"; | |
[@bs.deriving abstract] | |
type jsMutationResponseData = {data: Js.Json.t}; | |
type js_update = (js_cache, jsMutationResponseData) => unit; | |
/* The JS options provided to the mutation function */ | |
[@bs.deriving abstract] | |
type jsMutationOptions = { | |
[@bs.optional] | |
variables: Js.Json.t, | |
[@bs.optional] | |
update: js_update, | |
}; | |
[@bs.deriving abstract] | |
type makeJSMutationProps = { | |
mutation: queryString, | |
[@bs.optional] | |
variables: Js.Json.t, | |
[@bs.optional] | |
update: js_update, | |
}; | |
/* The JS function used to trigger the mutation */ | |
type js_mutate = | |
jsMutationOptions => | |
Repromise.Rejectable.t(jsMutationResponseData, ApolloError.jsError); | |
/* The JS object provided to the child function */ | |
type js_mutation_result = { | |
. | |
"loading": bool, | |
"data": Js.Nullable.t(Js.Json.t), | |
"error": Js.Nullable.t(ApolloError.jsError), | |
"called": bool, | |
}; | |
module type MutationDef = { | |
/* The variables used for the mutation */ | |
type variables; | |
/* The result of the mutation */ | |
type data; | |
/* A function to decode the JSON result to the result type */ | |
let decodeResult: Js.Json.t => data; | |
/* A function to encode the variable type as JSON */ | |
let encodeVariables: variables => Js.Json.t; | |
}; | |
module type Mutation = { | |
include MutationDef; | |
type result = Result.t(data, ApolloError.t); | |
type state = | |
| Uncalled | |
| Loading | |
| Done(result); | |
type mutate = | |
(~variables: variables=?, ~update: updateFunc(data)=?, unit) => | |
Repromise.t(result); | |
let make: | |
( | |
~mutation: ApolloTypes.queryString, | |
~variables: variables=?, | |
~update: updateFunc(data)=?, | |
(mutate, state) => ReasonReact.reactElement | |
) => | |
ReasonReact.component( | |
ReasonReact.stateless, | |
ReasonReact.noRetainedProps, | |
ReasonReact.actionless, | |
); | |
}; | |
module Make = (Definition: MutationDef) => { | |
include Definition; | |
type result = Result.t(data, ApolloError.t); | |
type state = | |
| Uncalled | |
| Loading | |
| Done(result); | |
/* The function provided as the first argument to the child function, | |
used to trigger the mutation */ | |
type mutate = | |
(~variables: Definition.variables=?, ~update: updateFunc(data)=?, unit) => | |
Repromise.t(result); | |
/* convert the JS result to a Reason one */ | |
let mapMutationResult: js_mutation_result => state = | |
jsResult => | |
switch ( | |
jsResult##called, | |
jsResult##loading, | |
jsResult##data |> Js.Nullable.toOption, | |
jsResult##error |> Js.Nullable.toOption, | |
) { | |
| (false, false, _, _) => Uncalled | |
| (_, true, _, _) => Loading | |
| (true, false, _, Some(err)) => | |
Done(Error(err |> ApolloError.mapError)) | |
| (true, false, Some(data), None) => Done(Ok(data |> decodeResult)) | |
/* This case is theoretically not possible, because it'd mean the mutation was called | |
but returned neither data nor an error. Not sure of a better way to handle this. */ | |
| (true, false, None, None) => Uncalled | |
}; | |
let updateFunction = (update, jsCache: js_cache, json) => { | |
let cache: cache = { | |
readQuery: (~query, ~variables=?, ()) => | |
jsCache##readQuery(makeJSReadQueryOptions(~query, ~variables?, ())), | |
writeQuery: (~query, ~data, ()) => | |
jsCache##writeQuery({"query": query, "data": data}), | |
}; | |
update(cache, json->dataGet |> decodeResult); | |
}; | |
let renderFunction = | |
(children, jsMutate: js_mutate, jsResult: js_mutation_result) => { | |
let mutate: mutate = | |
(~variables: option(variables)=?, ~update=?, _) => | |
jsMutationOptions( | |
~variables=?variables->(Option.map(encodeVariables)), | |
~update=?update->(Option.map(updateFunction)), | |
(), | |
) | |
|> jsMutate | |
|> Repromise.Rejectable.map(res => | |
Result.Ok(res->dataGet |> decodeResult) | |
) | |
|> Repromise.Rejectable.catch(err => | |
Repromise.resolved(Result.Error(err |> ApolloError.mapError)) | |
); | |
let child: ReasonReact.reactElement = | |
children(mutate, jsResult |> mapMutationResult); | |
child; | |
}; | |
let make = (~mutation, ~variables=?, ~update=?, children) => | |
ReasonReact.wrapJsForReason( | |
~reactClass=reactApolloMutation, | |
~props= | |
makeJSMutationProps( | |
~mutation, | |
~variables=?variables->(Option.map(encodeVariables)), | |
~update=?update->(Option.map(updateFunction)), | |
(), | |
), | |
children |> renderFunction, | |
); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
open Belt; | |
[@bs.module "react-apollo"] | |
external reactApolloQuery: ReasonReact.reactClass = "Query"; | |
let emptyObject = Json.Encode.([] |> object_); | |
type js_refetch_result = { | |
. | |
"data": Js.Null_undefined.t(Js.Json.t), | |
"error": Js.Nullable.t(ApolloError.jsError), | |
}; | |
type js_query_result('variables) = { | |
. | |
"loading": bool, | |
"data": Js.Null_undefined.t(Js.Json.t), | |
"error": Js.Nullable.t(ApolloError.jsError), | |
"refetch": | |
[@bs.meth] ( | |
Js.Null_undefined.t('variables) => | |
Repromise.Rejectable.t(js_refetch_result, ApolloError.jsError) | |
), | |
"networkStatus": int, | |
"variables": Js.Null_undefined.t(Js.Json.t), | |
}; | |
[@bs.deriving jsConverter] | |
type networkStatus = | |
| [@bs.as 1] Loading | |
| [@bs.as 2] SetVariables | |
| [@bs.as 3] FetchMore | |
| [@bs.as 4] Refetch | |
| [@bs.as 6] Poll | |
| [@bs.as 7] Ready | |
| [@bs.as 8] Error; | |
module type QueryConfig = { | |
type variables; | |
let query: ApolloTypes.queryString; | |
}; | |
module Make = (Config: QueryConfig) => { | |
type refetchResponse = { | |
data: option(Graphql.Query.t), | |
error: option(ApolloError.t), | |
}; | |
type response = { | |
data: option(Graphql.Query.t), | |
error: option(ApolloError.t), | |
loading: bool, | |
networkStatus: int, | |
refetch: | |
(~variables: Config.variables=?, unit) => | |
Repromise.t(Result.t(refetchResponse, ApolloError.t)), | |
}; | |
let mapData = d => | |
switch (d |> Js.Null_undefined.toOption) { | |
| None => None | |
| Some(data) => | |
/* result.data is an empty object before the data finishes loading. we want | |
to also treat that as a `None` condition */ | |
data == emptyObject ? None : Some(data) | |
}; | |
let mapError = e => | |
(e |> Js.Nullable.toOption)->Option.map(ApolloError.mapError); | |
let resultMapper: js_query_result(Config.variables) => response = | |
result => { | |
error: result##error |> mapError, | |
loading: result##loading, | |
data: result##data |> mapData, | |
networkStatus: result##networkStatus, | |
refetch: (~variables=?, ()) => | |
result##refetch(variables |> Js.Null_undefined.fromOption) | |
|> Repromise.Rejectable.map(res => | |
Result.Ok({ | |
data: res##data |> mapData, | |
error: res##error |> mapError, | |
}) | |
) | |
|> Repromise.Rejectable.catch(err => | |
Repromise.resolved(Result.Error(err |> ApolloError.mapError)) | |
), | |
}; | |
let make = | |
( | |
~variables: option(Config.variables)=?, | |
~ssr: bool=true, | |
~notifyOnNetworkStatusChange: bool=false, | |
children: response => ReasonReact.reactElement, | |
) => | |
ReasonReact.wrapJsForReason( | |
~reactClass=reactApolloQuery, | |
~props={ | |
"query": Config.query, | |
"variables": variables, | |
"ssr": ssr, | |
"notifyOnNetworkStatusChange": notifyOnNetworkStatusChange, | |
}, | |
result => | |
result |> resultMapper |> children | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment