@hyper-fetch/graphql 7.5.2 → 8.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.
Files changed (48) hide show
  1. package/{.eslintrc.json → .eslintrc copy.json } +1 -1
  2. package/.eslintrc.cjs +12 -0
  3. package/__tests__/constants/mutations.constants.ts +15 -0
  4. package/__tests__/constants/queries.constants.ts +24 -0
  5. package/__tests__/e2e/e2e.graphql-server.ts +135 -0
  6. package/__tests__/e2e/e2e.graphql.spec.ts +486 -0
  7. package/__tests__/features/adapter/adapter.browser.spec.ts +259 -0
  8. package/__tests__/features/adapter/adapter.server.spec.ts +234 -0
  9. package/__tests__/features/adapter/adapter.utils.spec.ts +86 -0
  10. package/__tests__/request-module.d.ts +12 -0
  11. package/__tests__/shared/callbacks.shared.ts +11 -0
  12. package/__tests__/shared/index.ts +2 -0
  13. package/__tests__/shared/progress.shared.ts +40 -0
  14. package/__tests__/tsconfig.json +23 -0
  15. package/__tests__/vitest.setup.ts +10 -0
  16. package/dist/adapter/adapter.constants.d.ts +4 -0
  17. package/dist/adapter/adapter.constants.d.ts.map +1 -0
  18. package/dist/adapter/adapter.d.ts +3 -0
  19. package/dist/adapter/adapter.d.ts.map +1 -0
  20. package/dist/adapter/adapter.types.d.ts +24 -0
  21. package/dist/adapter/adapter.types.d.ts.map +1 -0
  22. package/dist/adapter/adapter.utils.d.ts +9 -0
  23. package/dist/adapter/adapter.utils.d.ts.map +1 -0
  24. package/dist/adapter/http-adapter.fetch.d.ts +3 -0
  25. package/dist/adapter/http-adapter.fetch.d.ts.map +1 -0
  26. package/dist/adapter/index.d.ts +6 -0
  27. package/dist/adapter/index.d.ts.map +1 -0
  28. package/dist/index.d.ts +2 -34
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.mjs +183 -0
  31. package/dist/index.mjs.map +1 -0
  32. package/package.json +10 -25
  33. package/project.json +4 -0
  34. package/src/adapter/adapter.constants.ts +1 -1
  35. package/src/adapter/adapter.ts +3 -8
  36. package/src/adapter/adapter.types.ts +7 -3
  37. package/src/adapter/adapter.utils.ts +8 -3
  38. package/src/adapter/http-adapter.fetch.ts +186 -0
  39. package/src/adapter/index.ts +1 -0
  40. package/tsconfig.base.json +2 -6
  41. package/tsconfig.json +2 -6
  42. package/vite.config.ts +45 -0
  43. package/dist/browser/index.js +0 -191
  44. package/dist/browser/index.js.map +0 -7
  45. package/dist/index.js +0 -330
  46. package/dist/index.js.map +0 -7
  47. package/src/adapter/http-adapter.browser.ts +0 -148
  48. package/src/adapter/http-adapter.server.ts +0 -148
@@ -7,5 +7,5 @@
7
7
  "react/jsx-props-no-spreading": 0,
8
8
  "react/require-default-props": 0
9
9
  },
10
- "ignorePatterns": ["dist/", "coverage/", "jest.config.ts", "/*.js"]
10
+ "ignorePatterns": ["dist/", "coverage/", "vite.config.ts", "/*.js"]
11
11
  }
package/.eslintrc.cjs ADDED
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ extends: ["plugin:@nx/react", "../../.eslintrc.js"],
3
+ parserOptions: {
4
+ tsconfigRootDir: __dirname,
5
+ project: ["./tsconfig.json", "./__tests__/tsconfig.json"],
6
+ },
7
+ rules: {
8
+ "react/jsx-props-no-spreading": 0,
9
+ "react/require-default-props": 0,
10
+ },
11
+ ignorePatterns: ["dist/", "coverage/", "vite.config.ts", "/*.js"],
12
+ };
@@ -0,0 +1,15 @@
1
+ import gql from "graphql-tag";
2
+
3
+ export const loginMutation = gql`
4
+ mutation Login($username: String!, $password: String!) {
5
+ login(username: $username, password: $password) {
6
+ username
7
+ password
8
+ }
9
+ }
10
+ `;
11
+
12
+ export type LoginMutationVariables = {
13
+ username: string;
14
+ password: string;
15
+ };
@@ -0,0 +1,24 @@
1
+ import gql from "graphql-tag";
2
+
3
+ export const getUserQueryString = `
4
+ query GetUser {
5
+ username {
6
+ username
7
+ firstName
8
+ }
9
+ }
10
+ `;
11
+
12
+ export const getUserQuery = gql`
13
+ query GetUser {
14
+ username {
15
+ username
16
+ firstName
17
+ }
18
+ }
19
+ `;
20
+
21
+ export type GetUserQueryResponse = {
22
+ username: string;
23
+ firstName: string;
24
+ };
@@ -0,0 +1,135 @@
1
+ import http from "http";
2
+ import type { GraphQLSchema } from "graphql";
3
+ import { graphql, buildSchema } from "graphql";
4
+
5
+ export type GraphQLServerOptions = {
6
+ typeDefs: string;
7
+ resolvers: Record<string, (...args: any[]) => any>;
8
+ delay?: number;
9
+ };
10
+
11
+ /**
12
+ * Lightweight real GraphQL HTTP server for E2E testing.
13
+ *
14
+ * Spins up an actual `http.createServer` that handles
15
+ * POST (JSON body) and GET (query-string) GraphQL requests,
16
+ * just like a production server would.
17
+ */
18
+ export const createE2EGraphQLServer = (options: GraphQLServerOptions) => {
19
+ let server: http.Server | null = null;
20
+ let port = 0;
21
+
22
+ const schema: GraphQLSchema = buildSchema(options.typeDefs);
23
+ const rootValue = options.resolvers;
24
+
25
+ const collectBody = (req: http.IncomingMessage): Promise<string> => {
26
+ return new Promise((resolve, reject) => {
27
+ const chunks: string[] = [];
28
+ req.setEncoding("utf8");
29
+ req.on("data", (chunk: string) => chunks.push(chunk));
30
+ req.on("end", () => resolve(chunks.join("")));
31
+ req.on("error", reject);
32
+ });
33
+ };
34
+
35
+ const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
36
+ res.setHeader("Access-Control-Allow-Origin", "*");
37
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
38
+
39
+ if (req.method === "OPTIONS") {
40
+ res.writeHead(204);
41
+ res.end();
42
+ return;
43
+ }
44
+
45
+ let query: string | undefined;
46
+ let variables: Record<string, any> | undefined;
47
+ let operationName: string | undefined;
48
+
49
+ try {
50
+ if (req.method === "POST") {
51
+ const body = await collectBody(req);
52
+ const parsed = JSON.parse(body);
53
+ query = parsed.query;
54
+ variables = parsed.variables;
55
+ operationName = parsed.operationName;
56
+ } else if (req.method === "GET") {
57
+ const url = new URL(req.url || "/", `http://localhost`);
58
+ query = url.searchParams.get("query") || undefined;
59
+ const varsStr = url.searchParams.get("variables");
60
+ variables = varsStr ? JSON.parse(varsStr) : undefined;
61
+ operationName = url.searchParams.get("operationName") || undefined;
62
+ }
63
+
64
+ if (!query) {
65
+ res.writeHead(400, { "Content-Type": "application/json" });
66
+ res.end(JSON.stringify({ errors: [{ message: "Missing query" }] }));
67
+ return;
68
+ }
69
+
70
+ const result = await graphql({
71
+ schema,
72
+ source: query,
73
+ rootValue,
74
+ variableValues: variables,
75
+ operationName,
76
+ });
77
+
78
+ if (options.delay) {
79
+ await new Promise<void>((r) => {
80
+ setTimeout(r, options.delay);
81
+ });
82
+ }
83
+
84
+ const responseBody = JSON.stringify(result);
85
+ res.writeHead(200, {
86
+ "Content-Type": "application/json",
87
+ "Content-Length": Buffer.byteLength(responseBody).toString(),
88
+ });
89
+ res.end(responseBody);
90
+ } catch (err: any) {
91
+ const errorBody = JSON.stringify({ errors: [{ message: err.message }] });
92
+ res.writeHead(500, {
93
+ "Content-Type": "application/json",
94
+ "Content-Length": Buffer.byteLength(errorBody).toString(),
95
+ });
96
+ res.end(errorBody);
97
+ }
98
+ };
99
+
100
+ const startServer = (): Promise<string> => {
101
+ return new Promise((resolve, reject) => {
102
+ server = http.createServer(handleRequest);
103
+
104
+ server.listen(0, "127.0.0.1", () => {
105
+ const addr = server!.address();
106
+ if (addr && typeof addr === "object") {
107
+ port = addr.port;
108
+ resolve(`http://127.0.0.1:${port}`);
109
+ } else {
110
+ reject(new Error("Failed to get server address"));
111
+ }
112
+ });
113
+
114
+ server.on("error", reject);
115
+ });
116
+ };
117
+
118
+ const stopServer = (): Promise<void> => {
119
+ return new Promise((resolve, reject) => {
120
+ if (!server) {
121
+ resolve();
122
+ return;
123
+ }
124
+ server.close((err) => {
125
+ server = null;
126
+ if (err) reject(err);
127
+ else resolve();
128
+ });
129
+ });
130
+ };
131
+
132
+ const getUrl = () => `http://127.0.0.1:${port}`;
133
+
134
+ return { startServer, stopServer, getUrl };
135
+ };
@@ -0,0 +1,486 @@
1
+ /**
2
+ * @vitest-environment node
3
+ */
4
+ import { Client, getErrorMessage } from "@hyper-fetch/core";
5
+
6
+ import { GraphqlAdapter, GraphqlMethod } from "../../src/adapter";
7
+ import type { GraphQlExtraType } from "../../src/adapter/adapter.types";
8
+ import { createE2EGraphQLServer } from "./e2e.graphql-server";
9
+
10
+ // ─── Schema & Resolvers ──────────────────────────────────────────────
11
+
12
+ const typeDefs = `
13
+ type User {
14
+ id: ID!
15
+ username: String!
16
+ firstName: String!
17
+ email: String
18
+ }
19
+
20
+ type AuthPayload {
21
+ token: String!
22
+ user: User!
23
+ }
24
+
25
+ type Query {
26
+ user(id: ID!): User
27
+ users: [User!]!
28
+ search(term: String!): [User!]!
29
+ }
30
+
31
+ type Mutation {
32
+ login(username: String!, password: String!): AuthPayload!
33
+ createUser(username: String!, firstName: String!, email: String): User!
34
+ deleteUser(id: ID!): Boolean!
35
+ }
36
+ `;
37
+
38
+ const usersDb: Array<{ id: string; username: string; firstName: string; email: string | null }> = [
39
+ { id: "1", username: "john", firstName: "John", email: "john@example.com" },
40
+ { id: "2", username: "jane", firstName: "Jane", email: null },
41
+ { id: "3", username: "bob", firstName: "Bob", email: "bob@example.com" },
42
+ ];
43
+
44
+ let nextId = 4;
45
+
46
+ const resolvers = {
47
+ user: ({ id }: { id: string }) => {
48
+ return usersDb.find((u) => u.id === id) || null;
49
+ },
50
+ users: () => usersDb,
51
+ search: ({ term }: { term: string }) => {
52
+ return usersDb.filter((u) => u.username.includes(term) || u.firstName.includes(term));
53
+ },
54
+ login: ({ username, password }: { username: string; password: string }) => {
55
+ if (password.length < 4) throw new Error("Password too short");
56
+ return {
57
+ token: `token-${username}`,
58
+ user: usersDb.find((u) => u.username === username) || { id: "0", username, firstName: username, email: null },
59
+ };
60
+ },
61
+ createUser: ({ username, firstName, email }: { username: string; firstName: string; email?: string }) => {
62
+ // eslint-disable-next-line no-plusplus
63
+ const user = { id: String(nextId++), username, firstName, email: email || null };
64
+ usersDb.push(user);
65
+ return user;
66
+ },
67
+ deleteUser: ({ id }: { id: string }) => {
68
+ const idx = usersDb.findIndex((u) => u.id === id);
69
+ if (idx === -1) return false;
70
+ usersDb.splice(idx, 1);
71
+ return true;
72
+ },
73
+ };
74
+
75
+ type GqlResponse<T> = { response: T; error: { message: string }[] };
76
+
77
+ // ─── Server Setup ────────────────────────────────────────────────────
78
+
79
+ const { startServer, stopServer } = createE2EGraphQLServer({ typeDefs, resolvers });
80
+
81
+ let baseUrl: string;
82
+
83
+ beforeAll(async () => {
84
+ baseUrl = await startServer();
85
+ });
86
+
87
+ afterAll(async () => {
88
+ await stopServer();
89
+ });
90
+
91
+ // ─── Tests ───────────────────────────────────────────────────────────
92
+
93
+ describe("E2E GraphQL [ Queries ]", () => {
94
+ it("should fetch a list of users", async () => {
95
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
96
+ const request = client.createRequest<
97
+ GqlResponse<{ users: { id: string; username: string; firstName: string }[] }>
98
+ >()({
99
+ endpoint: "query { users { id username firstName } }",
100
+ });
101
+
102
+ const { data, error, status } = await request.send();
103
+
104
+ expect(error).toBeNull();
105
+ expect(status).toBe(200);
106
+ expect(data).toBeDefined();
107
+ expect(data!.users.length).toBeGreaterThanOrEqual(3);
108
+ expect(data!.users[0]).toHaveProperty("username");
109
+ expect(data!.users[0]).toHaveProperty("firstName");
110
+ });
111
+
112
+ it("should fetch a single user by ID with variables", async () => {
113
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
114
+ const request = client.createRequest<
115
+ GqlResponse<{ user: { id: string; username: string; firstName: string; email: string | null } }> & {
116
+ payload: { id: string };
117
+ }
118
+ >()({
119
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { id username firstName email } }`,
120
+ });
121
+
122
+ const { data, error, status } = await request.send({ payload: { id: "1" } } as any);
123
+
124
+ expect(error).toBeNull();
125
+ expect(status).toBe(200);
126
+ expect(data!.user).toStrictEqual({
127
+ id: "1",
128
+ username: "john",
129
+ firstName: "John",
130
+ email: "john@example.com",
131
+ });
132
+ });
133
+
134
+ it("should return null for non-existent user", async () => {
135
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
136
+ const request = client.createRequest<
137
+ GqlResponse<{ user: { id: string; username: string } | null }> & { payload: { id: string } }
138
+ >()({
139
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { id username } }`,
140
+ });
141
+
142
+ const { data, error, status } = await request.send({ payload: { id: "999" } } as any);
143
+
144
+ expect(error).toBeNull();
145
+ expect(status).toBe(200);
146
+ expect(data!.user).toBeNull();
147
+ });
148
+
149
+ it("should perform search query with string variable", async () => {
150
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
151
+ const request = client.createRequest<
152
+ GqlResponse<{ search: { username: string }[] }> & { payload: { term: string } }
153
+ >()({
154
+ endpoint: `query Search($term: String!) { search(term: $term) { username } }`,
155
+ });
156
+
157
+ const { data, error } = await request.send({ payload: { term: "john" } } as any);
158
+
159
+ expect(error).toBeNull();
160
+ expect(data!.search).toStrictEqual([{ username: "john" }]);
161
+ });
162
+
163
+ it("should work with GET method via query string", async () => {
164
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
165
+ const request = client.createRequest<GqlResponse<{ users: { id: string; username: string }[] }>>()({
166
+ endpoint: `query { users { id username } }`,
167
+ method: GraphqlMethod.GET,
168
+ });
169
+
170
+ const { data, error, status } = await request.send();
171
+
172
+ expect(error).toBeNull();
173
+ expect(status).toBe(200);
174
+ expect(data!.users.length).toBeGreaterThanOrEqual(3);
175
+ });
176
+
177
+ it("should fetch partial fields from user", async () => {
178
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
179
+ const request = client.createRequest<GqlResponse<{ user: { username: string } }> & { payload: { id: string } }>()({
180
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { username } }`,
181
+ });
182
+
183
+ const { data, error } = await request.send({ payload: { id: "1" } } as any);
184
+
185
+ expect(error).toBeNull();
186
+ expect(data!.user).toStrictEqual({ username: "john" });
187
+ });
188
+ });
189
+
190
+ describe("E2E GraphQL [ Mutations ]", () => {
191
+ it("should perform login mutation", async () => {
192
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
193
+ const request = client.createRequest<
194
+ GqlResponse<{ login: { token: string; user: { username: string } } }> & {
195
+ payload: { username: string; password: string };
196
+ }
197
+ >()({
198
+ endpoint: `mutation Login($username: String!, $password: String!) {
199
+ login(username: $username, password: $password) {
200
+ token
201
+ user { username }
202
+ }
203
+ }`,
204
+ });
205
+
206
+ const { data, error, status } = await request.send({
207
+ payload: { username: "john", password: "secret123" },
208
+ } as any);
209
+
210
+ expect(error).toBeNull();
211
+ expect(status).toBe(200);
212
+ expect(data!.login.token).toBe("token-john");
213
+ expect(data!.login.user.username).toBe("john");
214
+ });
215
+
216
+ it("should handle mutation validation errors from resolver", async () => {
217
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
218
+ const request = client.createRequest<
219
+ GqlResponse<{ login: { token: string } }> & { payload: { username: string; password: string } }
220
+ >()({
221
+ endpoint: `mutation Login($username: String!, $password: String!) {
222
+ login(username: $username, password: $password) { token }
223
+ }`,
224
+ });
225
+
226
+ const { data, error, status } = await request.send({
227
+ payload: { username: "john", password: "ab" },
228
+ } as any);
229
+
230
+ expect(status).toBe(200);
231
+ expect(data).toBeNull();
232
+ expect(error).toBeDefined();
233
+ const errors = error as { message: string }[];
234
+ expect(errors[0]).toHaveProperty("message");
235
+ expect(errors[0].message).toContain("Password too short");
236
+ });
237
+
238
+ it("should create a new user", async () => {
239
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
240
+ const request = client.createRequest<
241
+ GqlResponse<{ createUser: { id: string; username: string; firstName: string; email: string | null } }> & {
242
+ payload: { username: string; firstName: string; email?: string };
243
+ }
244
+ >()({
245
+ endpoint: `mutation CreateUser($username: String!, $firstName: String!, $email: String) {
246
+ createUser(username: $username, firstName: $firstName, email: $email) {
247
+ id username firstName email
248
+ }
249
+ }`,
250
+ });
251
+
252
+ const { data, error } = await request.send({
253
+ payload: { username: "alice", firstName: "Alice", email: "alice@test.com" },
254
+ } as any);
255
+
256
+ expect(error).toBeNull();
257
+ expect(data!.createUser.username).toBe("alice");
258
+ expect(data!.createUser.firstName).toBe("Alice");
259
+ expect(data!.createUser.email).toBe("alice@test.com");
260
+ expect(data!.createUser.id).toBeDefined();
261
+ });
262
+
263
+ it("should create a user without optional email", async () => {
264
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
265
+ const request = client.createRequest<
266
+ GqlResponse<{ createUser: { id: string; username: string; email: string | null } }> & {
267
+ payload: { username: string; firstName: string };
268
+ }
269
+ >()({
270
+ endpoint: `mutation CreateUser($username: String!, $firstName: String!, $email: String) {
271
+ createUser(username: $username, firstName: $firstName, email: $email) {
272
+ id username email
273
+ }
274
+ }`,
275
+ });
276
+
277
+ const { data, error } = await request.send({
278
+ payload: { username: "noEmail", firstName: "No Email" },
279
+ } as any);
280
+
281
+ expect(error).toBeNull();
282
+ expect(data!.createUser.email).toBeNull();
283
+ });
284
+
285
+ it("should delete a user", async () => {
286
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
287
+ const request = client.createRequest<GqlResponse<{ deleteUser: boolean }> & { payload: { id: string } }>()({
288
+ endpoint: `mutation DeleteUser($id: ID!) { deleteUser(id: $id) }`,
289
+ });
290
+
291
+ const { data, error } = await request.send({ payload: { id: "3" } } as any);
292
+
293
+ expect(error).toBeNull();
294
+ expect(data!.deleteUser).toBe(true);
295
+ });
296
+
297
+ it("should return false when deleting non-existent user", async () => {
298
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
299
+ const request = client.createRequest<GqlResponse<{ deleteUser: boolean }> & { payload: { id: string } }>()({
300
+ endpoint: `mutation DeleteUser($id: ID!) { deleteUser(id: $id) }`,
301
+ });
302
+
303
+ const { data, error } = await request.send({ payload: { id: "999" } } as any);
304
+
305
+ expect(error).toBeNull();
306
+ expect(data!.deleteUser).toBe(false);
307
+ });
308
+ });
309
+
310
+ describe("E2E GraphQL [ Error Handling ]", () => {
311
+ it("should handle invalid GraphQL syntax", async () => {
312
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
313
+ const request = client.createRequest<GqlResponse<any>>()({
314
+ endpoint: `query { invalid { `,
315
+ });
316
+
317
+ const { data, error, status } = await request.send();
318
+
319
+ expect(status).toBe(200);
320
+ expect(data).toBeNull();
321
+ expect(error).toBeDefined();
322
+ expect((error as { message: string }[]).length).toBeGreaterThan(0);
323
+ });
324
+
325
+ it("should handle queries against non-existent fields", async () => {
326
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
327
+ const request = client.createRequest<GqlResponse<any>>()({
328
+ endpoint: `query { nonExistentField { id } }`,
329
+ });
330
+
331
+ const { data, error } = await request.send();
332
+
333
+ expect(data).toBeNull();
334
+ expect(error).toBeDefined();
335
+ });
336
+
337
+ it("should handle missing required variables", async () => {
338
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
339
+ const request = client.createRequest<GqlResponse<{ user: any }>>()({
340
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { id } }`,
341
+ });
342
+
343
+ const { data, error } = await request.send({} as any);
344
+
345
+ expect(data).toBeNull();
346
+ expect(error).toBeDefined();
347
+ expect((error as { message: string }[]).length).toBeGreaterThan(0);
348
+ });
349
+
350
+ it("should handle abort/cancel of request", async () => {
351
+ const delayed = createE2EGraphQLServer({ typeDefs, resolvers, delay: 50 });
352
+ const delayedUrl = await delayed.startServer();
353
+
354
+ try {
355
+ const client = new Client({ url: delayedUrl }).setAdapter(GraphqlAdapter());
356
+ const request = client.createRequest<GqlResponse<any>>()({
357
+ endpoint: `query { users { id } }`,
358
+ });
359
+
360
+ setTimeout(() => request.abort(), 2);
361
+
362
+ const { data, error } = await request.send();
363
+
364
+ expect(data).toBeNull();
365
+ expect(error).toStrictEqual([getErrorMessage("abort")]);
366
+ } finally {
367
+ await delayed.stopServer();
368
+ }
369
+ });
370
+ });
371
+
372
+ describe("E2E GraphQL [ Headers & Extras ]", () => {
373
+ it("should include response headers in extra", async () => {
374
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
375
+ const request = client.createRequest<GqlResponse<any>>()({
376
+ endpoint: `query { users { id } }`,
377
+ });
378
+
379
+ const { extra } = await request.send();
380
+
381
+ expect(extra).toBeDefined();
382
+ const typedExtra = extra as GraphQlExtraType;
383
+ expect(typedExtra.headers).toBeDefined();
384
+ expect((typedExtra.headers as Record<string, string>)["content-type"]).toBe("application/json");
385
+ });
386
+
387
+ it("should pass custom headers to the server", async () => {
388
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
389
+ const request = client
390
+ .createRequest<GqlResponse<any>>()({
391
+ endpoint: `query { users { id } }`,
392
+ })
393
+ .setHeaders({ Authorization: "Bearer test-token" });
394
+
395
+ const { data, error, status } = await request.send();
396
+
397
+ expect(error).toBeNull();
398
+ expect(status).toBe(200);
399
+ expect(data).toBeDefined();
400
+ });
401
+
402
+ it("should include extensions in extra when present", async () => {
403
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
404
+ const request = client.createRequest<GqlResponse<any>>()({
405
+ endpoint: `query { users { id } }`,
406
+ });
407
+
408
+ const { extra } = await request.send();
409
+
410
+ expect(extra).toBeDefined();
411
+ expect((extra as GraphQlExtraType).extensions).toBeDefined();
412
+ });
413
+
414
+ it("should include content-length in response headers", async () => {
415
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
416
+ const request = client.createRequest<GqlResponse<any>>()({
417
+ endpoint: `query { users { id } }`,
418
+ });
419
+
420
+ const { extra } = await request.send();
421
+
422
+ const headers = (extra as GraphQlExtraType).headers as Record<string, string>;
423
+ expect(headers["content-length"]).toBeDefined();
424
+ expect(Number(headers["content-length"])).toBeGreaterThan(0);
425
+ });
426
+ });
427
+
428
+ describe("E2E GraphQL [ Multiple Operations ]", () => {
429
+ it("should handle named operations", async () => {
430
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
431
+ const request = client.createRequest<GqlResponse<{ users: { id: string; username: string }[] }>>()({
432
+ endpoint: `query ListUsers { users { id username } }`,
433
+ });
434
+
435
+ const { data, error } = await request.send();
436
+
437
+ expect(error).toBeNull();
438
+ expect(data!.users.length).toBeGreaterThanOrEqual(2);
439
+ });
440
+
441
+ it("should handle sequential requests sharing a client", async () => {
442
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
443
+
444
+ const listRequest = client.createRequest<GqlResponse<{ users: { id: string }[] }>>()({
445
+ endpoint: `query { users { id } }`,
446
+ });
447
+
448
+ const getRequest = client.createRequest<
449
+ GqlResponse<{ user: { id: string; username: string } }> & { payload: { id: string } }
450
+ >()({
451
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { id username } }`,
452
+ });
453
+
454
+ const listResult = await listRequest.send();
455
+ expect(listResult.error).toBeNull();
456
+ expect(listResult.data!.users.length).toBeGreaterThanOrEqual(2);
457
+
458
+ const firstUserId = listResult.data!.users[0].id;
459
+ const getResult = await getRequest.send({ payload: { id: firstUserId } } as any);
460
+ expect(getResult.error).toBeNull();
461
+ expect(getResult.data!.user.id).toBe(firstUserId);
462
+ });
463
+
464
+ it("should handle concurrent requests", async () => {
465
+ const client = new Client({ url: baseUrl }).setAdapter(GraphqlAdapter());
466
+
467
+ const req1 = client
468
+ .createRequest<GqlResponse<{ user: { username: string } }> & { payload: { id: string } }>()({
469
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { username } }`,
470
+ })
471
+ .send({ payload: { id: "1" } } as any);
472
+
473
+ const req2 = client
474
+ .createRequest<GqlResponse<{ user: { username: string } }> & { payload: { id: string } }>()({
475
+ endpoint: `query GetUser($id: ID!) { user(id: $id) { username } }`,
476
+ })
477
+ .send({ payload: { id: "2" } } as any);
478
+
479
+ const [result1, result2] = await Promise.all([req1, req2]);
480
+
481
+ expect(result1.error).toBeNull();
482
+ expect(result2.error).toBeNull();
483
+ expect(result1.data!.user.username).toBe("john");
484
+ expect(result2.data!.user.username).toBe("jane");
485
+ });
486
+ });