@hyper-fetch/graphql 1.0.0

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.
@@ -0,0 +1,90 @@
1
+ import { getAdapterBindings, getResponseHeaders, parseResponse, parseErrorResponse } from "@hyper-fetch/core";
2
+
3
+ import { gqlExtra, GraphQLAdapterType, defaultTimeout, getRequestValues } from "adapter";
4
+
5
+ export const adapter: GraphQLAdapterType = async (request, requestId) => {
6
+ const {
7
+ makeRequest,
8
+ config,
9
+ headers,
10
+ onError,
11
+ onResponseEnd,
12
+ onTimeoutError,
13
+ onRequestEnd,
14
+ createAbortListener,
15
+ onResponseProgress,
16
+ onRequestProgress,
17
+ onResponseStart,
18
+ onBeforeRequest,
19
+ onRequestStart,
20
+ onSuccess,
21
+ } = await getAdapterBindings<GraphQLAdapterType>(request, requestId, 0, gqlExtra);
22
+
23
+ const { fullUrl, payload, method } = getRequestValues(request);
24
+
25
+ return makeRequest((resolve) => {
26
+ const xhr = new XMLHttpRequest();
27
+ xhr.timeout = defaultTimeout;
28
+
29
+ const abort = () => xhr.abort();
30
+
31
+ // Inject xhr options
32
+ Object.entries(config).forEach(([name, value]) => {
33
+ xhr[name] = value;
34
+ });
35
+
36
+ // Open connection
37
+ xhr.open(method, fullUrl, true);
38
+
39
+ // Set Headers
40
+ Object.entries(headers).forEach(([name, value]) => xhr.setRequestHeader(name, value as string));
41
+
42
+ // Listen to abort signal
43
+ const unmountListener = createAbortListener(0, gqlExtra, abort, resolve);
44
+
45
+ // Request handlers
46
+ xhr.upload.onprogress = onRequestProgress;
47
+
48
+ // Response handlers
49
+ xhr.onloadstart = (): void => {
50
+ onRequestEnd();
51
+ onResponseStart();
52
+ };
53
+
54
+ xhr.onprogress = onResponseProgress;
55
+
56
+ xhr.onloadend = () => {
57
+ onResponseEnd();
58
+ unmountListener();
59
+ };
60
+
61
+ xhr.ontimeout = () => onTimeoutError(0, gqlExtra, resolve);
62
+
63
+ // Data handler
64
+ xhr.onreadystatechange = (e: Event) => {
65
+ const event = e as unknown as ProgressEvent<XMLHttpRequest>;
66
+ const finishedState = 4;
67
+
68
+ if (event.target && event.target.readyState === finishedState) {
69
+ const { status } = event.target;
70
+ const success = String(status).startsWith("2") || String(status).startsWith("3");
71
+ const responseHeaders = getResponseHeaders(xhr.getAllResponseHeaders());
72
+
73
+ if (success) {
74
+ const data = parseResponse(event.target.response);
75
+ onSuccess(data, status, { headers: responseHeaders }, resolve);
76
+ } else {
77
+ // delay to finish after onabort/ontimeout
78
+ const data = parseErrorResponse(event.target.response);
79
+ onError(data, status, { headers: responseHeaders }, resolve);
80
+ }
81
+ }
82
+ };
83
+
84
+ // Start request
85
+ onBeforeRequest();
86
+ onRequestStart();
87
+
88
+ xhr.send(payload);
89
+ });
90
+ };
@@ -0,0 +1,9 @@
1
+ import { DateInterval } from "@hyper-fetch/core";
2
+
3
+ import { GraphQlExtraType } from "./adapter.types";
4
+
5
+ export const defaultTimeout = DateInterval.second * 5;
6
+
7
+ export const gqlExtra: GraphQlExtraType = {
8
+ headers: {},
9
+ };
@@ -0,0 +1,91 @@
1
+ import { getAdapterBindings, parseErrorResponse, parseResponse } from "@hyper-fetch/core";
2
+ import http, { OutgoingHttpHeaders } from "http";
3
+ import https from "https";
4
+
5
+ import { gqlExtra, defaultTimeout, GraphQLAdapterType, getRequestValues } from "adapter";
6
+
7
+ export const adapter: GraphQLAdapterType = async (request, requestId) => {
8
+ const {
9
+ makeRequest,
10
+ config,
11
+ headers,
12
+ onError,
13
+ onResponseEnd,
14
+ onTimeoutError,
15
+ onRequestEnd,
16
+ createAbortListener,
17
+ onResponseProgress,
18
+ onResponseStart,
19
+ onBeforeRequest,
20
+ onRequestStart,
21
+ onSuccess,
22
+ } = await getAdapterBindings<GraphQLAdapterType>(request, requestId, 0, gqlExtra);
23
+
24
+ const { fullUrl, payload, method } = getRequestValues(request);
25
+
26
+ const httpClient = request.client.url.includes("https://") ? https : http;
27
+ const options = {
28
+ path: fullUrl,
29
+ method,
30
+ headers: headers as OutgoingHttpHeaders,
31
+ timeout: defaultTimeout,
32
+ };
33
+
34
+ Object.entries(config).forEach(([name, value]) => {
35
+ options[name] = value;
36
+ });
37
+
38
+ let unmountListener = () => null;
39
+ onBeforeRequest();
40
+
41
+ if (payload) {
42
+ options.headers["Content-Length"] = Buffer.byteLength(JSON.stringify(payload));
43
+ }
44
+
45
+ return makeRequest((resolve) => {
46
+ const httpRequest = httpClient.request(options, (response) => {
47
+ response.setEncoding("utf8");
48
+ unmountListener = createAbortListener(0, gqlExtra, response.destroy, resolve);
49
+
50
+ let chunks = "";
51
+ const totalDownloadBytes = Number(response.headers["content-length"]);
52
+ let downloadedBytes = 0;
53
+
54
+ onRequestStart();
55
+
56
+ response.on("data", (chunk) => {
57
+ if (!chunks) {
58
+ onRequestEnd();
59
+ onResponseStart();
60
+ }
61
+ downloadedBytes += chunk.length;
62
+ chunks += chunk;
63
+ onResponseProgress({ total: totalDownloadBytes, loaded: downloadedBytes });
64
+ });
65
+
66
+ response.on("end", () => {
67
+ const { statusCode } = response;
68
+ const success = String(statusCode).startsWith("2") || String(statusCode).startsWith("3");
69
+
70
+ if (success) {
71
+ const data = parseResponse(chunks);
72
+ onSuccess(data, statusCode, { headers: response.headers as Record<string, string> }, resolve);
73
+ } else {
74
+ // delay to finish after onabort/ontimeout
75
+ const data = parseErrorResponse(chunks);
76
+ onError(data, statusCode, { headers: response.headers as Record<string, string> }, resolve);
77
+ }
78
+
79
+ unmountListener();
80
+ onResponseEnd();
81
+ });
82
+ });
83
+
84
+ httpRequest.on("timeout", () => onTimeoutError(0, gqlExtra, resolve));
85
+ httpRequest.on("error", (error) => onError(error, 0, gqlExtra, resolve));
86
+ if (payload) {
87
+ httpRequest.write(payload);
88
+ }
89
+ httpRequest.end();
90
+ });
91
+ };
@@ -0,0 +1,27 @@
1
+ import * as browser from "@browser-adapter";
2
+ import * as server from "@server-adapter";
3
+ import { HttpMethodsEnum, ClientInstance } from "@hyper-fetch/core";
4
+ import { print } from "graphql/language/printer";
5
+
6
+ import { GraphQLAdapterType, GraphQlEndpointType } from "adapter";
7
+
8
+ export const graphqlAdapter = (client: ClientInstance) => {
9
+ const adapter: GraphQLAdapterType = async (request, requestId) => {
10
+ if (typeof XMLHttpRequest !== "undefined") {
11
+ return browser.adapter(request, requestId);
12
+ }
13
+ return server.adapter(request, requestId);
14
+ };
15
+
16
+ const mapper = <E extends GraphQlEndpointType>(endpoint: E): string => {
17
+ if (typeof endpoint === "string") {
18
+ return endpoint;
19
+ }
20
+ return print(endpoint);
21
+ };
22
+
23
+ return client
24
+ .setDefaultMethod(HttpMethodsEnum.post)
25
+ .setEndpointMapper(mapper)
26
+ .setAdapter(() => adapter);
27
+ };
@@ -0,0 +1,54 @@
1
+ import { AdapterType, NegativeTypes, QueryParamsType } from "@hyper-fetch/core";
2
+ import { DocumentNode } from "graphql/language/ast";
3
+
4
+ export enum GraphQlMethod {
5
+ POST = "POST",
6
+ GET = "GET",
7
+ }
8
+
9
+ export type GraphQlExtraType = { headers: HeadersInit };
10
+
11
+ export type GraphQlEndpointType = string | DocumentNode;
12
+
13
+ export type GraphQLAdapterType = AdapterType<
14
+ Partial<XMLHttpRequest>,
15
+ GraphQlMethod,
16
+ number,
17
+ GraphQlExtraType,
18
+ QueryParamsType | string,
19
+ GraphQlEndpointType
20
+ >;
21
+
22
+ // Extract
23
+
24
+ type GqlParamType = boolean | string | number | Record<string, any> | NegativeTypes;
25
+
26
+ export type ExtractGqlRouteParams<T extends string> = string extends T
27
+ ? NegativeTypes
28
+ : T extends `${string}query ${infer Parameters} {${infer QueryRest}`
29
+ ? Parameters extends `${string}$${infer Param},${infer Rest}`
30
+ ? GqlParameter<Param, Rest> &
31
+ // eslint-disable-next-line @typescript-eslint/ban-types
32
+ (ExtractGqlRouteParams<QueryRest> extends NegativeTypes ? {} : ExtractGqlRouteParams<QueryRest>)
33
+ : Parameters extends `${string}$${infer Param})${infer Rest}`
34
+ ? GqlParameter<Param, Rest> &
35
+ // eslint-disable-next-line @typescript-eslint/ban-types
36
+ (ExtractGqlRouteParams<QueryRest> extends NegativeTypes ? {} : ExtractGqlRouteParams<QueryRest>)
37
+ : NegativeTypes
38
+ : T extends `${string}$${infer Parameters},${infer Rest}`
39
+ ? GqlParameter<Parameters, Rest>
40
+ : T extends `${string}$${infer Parameters})${infer Rest}`
41
+ ? GqlParameter<Parameters, Rest>
42
+ : NegativeTypes;
43
+
44
+ export type GqlParameter<T extends string, Rest extends string> = T extends `${infer Param}:${string}!`
45
+ ? { [k in Param]: GqlParamType } & (ExtractGqlRouteParams<Rest> extends NegativeTypes
46
+ ? // eslint-disable-next-line @typescript-eslint/ban-types
47
+ {}
48
+ : ExtractGqlRouteParams<Rest>)
49
+ : T extends `${infer Param}:${string}`
50
+ ? { [k in Param]?: GqlParamType } & (ExtractGqlRouteParams<Rest> extends NegativeTypes
51
+ ? // eslint-disable-next-line @typescript-eslint/ban-types
52
+ {}
53
+ : ExtractGqlRouteParams<Rest>)
54
+ : NegativeTypes;
@@ -0,0 +1,23 @@
1
+ import { RequestInstance, HttpMethodsEnum } from "@hyper-fetch/core";
2
+
3
+ export const getRequestValues = (request: RequestInstance) => {
4
+ const { method } = request;
5
+
6
+ const isPostRequest = request.method === HttpMethodsEnum.post;
7
+
8
+ const query = request.endpoint;
9
+ const variables = request.data;
10
+
11
+ const fullUrl = isPostRequest
12
+ ? request.client.url
13
+ : request.client.url + request.client.stringifyQueryParams({ query, variables });
14
+
15
+ const payload = isPostRequest
16
+ ? JSON.stringify({
17
+ query,
18
+ variables,
19
+ })
20
+ : null;
21
+
22
+ return { fullUrl, payload, method };
23
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./adapter";
2
+ export * from "./adapter.types";
3
+ export * from "./adapter.utils";
4
+ export * from "./adapter.constants";
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ // rollup client entry point
2
+
3
+ export * from "./adapter";
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "baseUrl": "./src",
6
+ "paths": {
7
+ "@browser-adapter": ["./adapter/adapter.browser.ts"],
8
+ "@server-adapter": ["./adapter/adapter.browser.ts"]
9
+ }
10
+ },
11
+ "include": ["./src/index.ts", "__tests__/*"],
12
+ "exclude": ["dist", "jest.config.ts"]
13
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "baseUrl": "./src",
6
+ "paths": {
7
+ "@browser-adapter": ["./adapter/adapter.browser.ts"],
8
+ "@server-adapter": ["./adapter/adapter.server.ts"]
9
+ }
10
+ },
11
+ "exclude": ["dist", "jest.config.ts"]
12
+ }