@uniflowed/graphql 0.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +113 -0
  2. package/package.json +25 -0
package/index.js ADDED
@@ -0,0 +1,113 @@
1
+ // @flow
2
+ //
3
+ // `@uniflowed/graphql`: the Relay environment, without the boilerplate.
4
+ //
5
+ // Relay itself is `@uniflowed/relay`, which is the real `react-relay`. This is
6
+ // the twenty lines every Relay application writes before it can use it: a
7
+ // network layer that posts an operation to an endpoint, a store, and an
8
+ // environment holding the two together.
9
+ //
10
+ // It exists because that boilerplate is not a design decision — every project
11
+ // writes the same thing, gets the response shape slightly wrong the first time,
12
+ // and discovers it when a GraphQL error is silently rendered as `null`. uf has
13
+ // an opinion about it, and the opinion is small enough to read.
14
+ //
15
+ // It is deliberately not a wrapper. `createEnvironment` hands back Relay's own
16
+ // `Environment`, so everything Relay can do a uf project can do, and a project
17
+ // that outgrows this function replaces the call rather than escaping a
18
+ // framework. That is red line 8, and this is what it looks like when it holds.
19
+
20
+ import type { FetchClient } from "@uniflowed/fetch";
21
+ import { Environment, Network, Observable, RecordSource, Store } from "relay-runtime";
22
+
23
+ export type { Environment } from "relay-runtime";
24
+
25
+ /** A GraphQL error as a server reports it. */
26
+ export type GraphQlError = {
27
+ readonly message: string,
28
+ readonly path?: $ReadOnlyArray<string | number>,
29
+ };
30
+
31
+ /** Raised when a response carries `errors`. */
32
+ export class GraphQlResponseError extends Error {
33
+ /** Every error the response reported, in the order it reported them. */
34
+ errors: $ReadOnlyArray<GraphQlError>;
35
+
36
+ constructor(errors: $ReadOnlyArray<GraphQlError>) {
37
+ const first = errors[0]?.message ?? "the server reported an error";
38
+ super(errors.length === 1 ? first : `${first} (and ${errors.length - 1} more)`);
39
+ this.name = "GraphQlResponseError";
40
+ this.errors = errors;
41
+ }
42
+ }
43
+
44
+ /** How to reach the GraphQL endpoint. */
45
+ export type EnvironmentOptions = {
46
+ /** Where operations are posted. */
47
+ readonly endpoint: string,
48
+ /**
49
+ * The fetch client to post with.
50
+ *
51
+ * Explicit rather than reaching for a global: uf does not override
52
+ * `globalThis.fetch`, and a server rendering a request often needs to forward
53
+ * that request's credentials, which a global cannot know about.
54
+ */
55
+ readonly fetch: FetchClient,
56
+ /** Headers sent with every operation, e.g. an authorization token. */
57
+ readonly headers?: { readonly [string]: string },
58
+ };
59
+
60
+ /**
61
+ * A Relay environment that posts operations to `endpoint`.
62
+ *
63
+ * The store is fresh, so two calls are two independent caches — which is what a
64
+ * server needs, where one environment per request is the only way two users do
65
+ * not see each other's data.
66
+ */
67
+ export function createEnvironment(options: EnvironmentOptions): Environment {
68
+ return new Environment({
69
+ network: Network.create(fetchOperation(options)),
70
+ store: new Store(new RecordSource()),
71
+ });
72
+ }
73
+
74
+ /**
75
+ * The network function Relay calls to run one operation.
76
+ *
77
+ * Returns a promise rather than an `Observable`: Relay accepts either, and a
78
+ * promise is the whole of what a plain HTTP endpoint can do. Subscriptions need
79
+ * the observable form and a transport that supports them, which is a different
80
+ * function and not one uf guesses at.
81
+ */
82
+ function fetchOperation(options: EnvironmentOptions) {
83
+ return async function run(operation: mixed, variables: mixed): Promise<mixed> {
84
+ const request = operation as $FlowFixMe;
85
+ // `raw` rather than `request`: a GraphQL endpoint answers 200 with an
86
+ // `errors` array, so the status is not the outcome and the body has to be
87
+ // read either way.
88
+ const response = await options.fetch.raw(options.endpoint, {
89
+ method: "POST",
90
+ headers: {
91
+ accept: "application/graphql-response+json, application/json",
92
+ ...(options.headers ?? {}),
93
+ },
94
+ body: {
95
+ query: request.text,
96
+ variables,
97
+ // Relay names every operation, and sending the name makes a server's
98
+ // logs and traces readable without any extra work.
99
+ operationName: request.name,
100
+ },
101
+ });
102
+
103
+ const payload = (await response.json()) as $FlowFixMe;
104
+ // A GraphQL response can carry `errors` and still be HTTP 200, and a
105
+ // client that only checks the status renders `null` and says nothing.
106
+ if (Array.isArray(payload?.errors) && payload.errors.length > 0) {
107
+ throw new GraphQlResponseError(payload.errors);
108
+ }
109
+ return payload;
110
+ };
111
+ }
112
+
113
+ export { Observable };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@uniflowed/graphql",
3
+ "version": "0.0.0-alpha.2",
4
+ "description": "The Relay environment, without the boilerplate, for the Unified Toolchain for Flow.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
+ "directory": "packages/graphql"
12
+ },
13
+ "exports": {
14
+ ".": "./index.js"
15
+ },
16
+ "files": [
17
+ "index.js"
18
+ ],
19
+ "dependencies": {
20
+ "@uniflowed/fetch": "0.0.0-alpha.2"
21
+ },
22
+ "peerDependencies": {
23
+ "relay-runtime": ">=20"
24
+ }
25
+ }