@graphql-tools/executor-http 1.1.14 → 1.2.0-alpha-753096403f92c5dbcdd17828acdd17a1aa53a51e

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @graphql-tools/executor-http
2
2
 
3
+ ## 1.2.0-alpha-753096403f92c5dbcdd17828acdd17a1aa53a51e
4
+
5
+ ### Minor Changes
6
+
7
+ - [#313](https://github.com/graphql-hive/gateway/pull/313) [`7530964`](https://github.com/graphql-hive/gateway/commit/753096403f92c5dbcdd17828acdd17a1aa53a51e) Thanks [@ardatan](https://github.com/ardatan)! - Automatic Persisted Queries support for upstream requests
8
+
9
+ For HTTP Executor;
10
+
11
+ ```ts
12
+ buildHTTPExecutor({
13
+ // ...
14
+ apq: true,
15
+ });
16
+ ```
17
+
18
+ For Gateway Configuration;
19
+
20
+ ```ts
21
+ export const gatewayConfig = defineConfig({
22
+ transportEntries: {
23
+ '*': {
24
+ options: {
25
+ apq: true,
26
+ },
27
+ },
28
+ },
29
+ });
30
+ ```
31
+
3
32
  ## 1.1.14
4
33
 
5
34
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -30,32 +30,21 @@ function collectAsyncIterableValues(asyncIterable) {
30
30
  }
31
31
  return iterate();
32
32
  }
33
- function createFormDataFromVariables({
34
- query,
35
- variables,
36
- operationName,
37
- extensions
38
- }, {
33
+ function createFormDataFromVariables(body, {
39
34
  File: FileCtor = fetch.File,
40
35
  FormData: FormDataCtor = fetch.FormData
41
36
  }) {
42
- const vars = Object.assign({}, variables);
37
+ if (!body.variables) {
38
+ return JSON.stringify(body);
39
+ }
40
+ const vars = Object.assign({}, body.variables);
43
41
  const { clone, files } = extractFiles.extractFiles(
44
42
  vars,
45
43
  "variables",
46
44
  (v) => extractFiles.isExtractableFile(v) || v?.promise || utils.isAsyncIterable(v) || v?.then || typeof v?.arrayBuffer === "function"
47
45
  );
48
46
  if (files.size === 0) {
49
- return JSON.stringify(
50
- {
51
- query,
52
- variables,
53
- operationName,
54
- extensions
55
- },
56
- null,
57
- 2
58
- );
47
+ return JSON.stringify(body);
59
48
  }
60
49
  const map = {};
61
50
  const uploads = [];
@@ -69,10 +58,8 @@ function createFormDataFromVariables({
69
58
  form.append(
70
59
  "operations",
71
60
  JSON.stringify({
72
- query,
73
- variables: clone,
74
- operationName,
75
- extensions
61
+ ...body,
62
+ variables: clone
76
63
  })
77
64
  );
78
65
  form.append("map", JSON.stringify(map));
@@ -150,6 +137,20 @@ function createResultForAbort(reason, extensions) {
150
137
  errors: [createGraphQLErrorForAbort(reason, extensions)]
151
138
  };
152
139
  }
140
+ function hashSHA256(str) {
141
+ const textEncoder = new fetch.TextEncoder();
142
+ const utf8 = textEncoder.encode(str);
143
+ return utils.mapMaybePromise(
144
+ fetch.crypto.subtle.digest("SHA-256", utf8),
145
+ (hashBuffer) => {
146
+ let hashHex = "";
147
+ for (const bytes of new Uint8Array(hashBuffer)) {
148
+ hashHex += bytes.toString(16).padStart(2, "0");
149
+ }
150
+ return hashHex;
151
+ }
152
+ );
153
+ }
153
154
 
154
155
  const DELIM = "\n\n";
155
156
  function isReadableStream(value) {
@@ -252,23 +253,22 @@ const isLiveQueryOperationDefinitionNode = utils.memoize1(
252
253
 
253
254
  function prepareGETUrl({
254
255
  baseUrl = "",
255
- query,
256
- variables,
257
- operationName,
258
- extensions
256
+ body
259
257
  }) {
260
258
  const dummyHostname = "https://dummyhostname.com";
261
259
  const validUrl = baseUrl.startsWith("http") ? baseUrl : baseUrl?.startsWith("/") ? `${dummyHostname}${baseUrl}` : `${dummyHostname}/${baseUrl}`;
262
260
  const urlObj = new URL(validUrl);
263
- urlObj.searchParams.set("query", graphql.stripIgnoredCharacters(query));
264
- if (variables && Object.keys(variables).length > 0) {
265
- urlObj.searchParams.set("variables", JSON.stringify(variables));
261
+ if (body.query) {
262
+ urlObj.searchParams.set("query", body.query);
263
+ }
264
+ if (body.variables && Object.keys(body.variables).length > 0) {
265
+ urlObj.searchParams.set("variables", JSON.stringify(body.variables));
266
266
  }
267
- if (operationName) {
268
- urlObj.searchParams.set("operationName", operationName);
267
+ if (body.operationName) {
268
+ urlObj.searchParams.set("operationName", body.operationName);
269
269
  }
270
- if (extensions) {
271
- urlObj.searchParams.set("extensions", JSON.stringify(extensions));
270
+ if (body.extensions) {
271
+ urlObj.searchParams.set("extensions", JSON.stringify(body.extensions));
272
272
  }
273
273
  const finalUrl = urlObj.toString().replace(dummyHostname, "");
274
274
  return finalUrl;
@@ -294,7 +294,7 @@ function buildHTTPExecutor(options) {
294
294
  const printFn = options?.print ?? defaultPrintFn;
295
295
  const disposeCtrl = new AbortController();
296
296
  const sharedSignal = createSignalWrapper(disposeCtrl.signal);
297
- const baseExecutor = (request) => {
297
+ const baseExecutor = (request, excludeQuery) => {
298
298
  if (sharedSignal.aborted) {
299
299
  return createResultForAbort(sharedSignal.reason);
300
300
  }
@@ -325,7 +325,6 @@ function buildHTTPExecutor(options) {
325
325
  Object.assign(headers, headersFromExtensions);
326
326
  request.extensions = restExtensions;
327
327
  }
328
- const query = printFn(request.document);
329
328
  let signal = sharedSignal;
330
329
  if (options?.timeout) {
331
330
  signal = AbortSignal.any([
@@ -339,163 +338,204 @@ function buildHTTPExecutor(options) {
339
338
  },
340
339
  response: {}
341
340
  };
342
- return new valueOrPromise.ValueOrPromise(() => {
343
- switch (method) {
344
- case "GET": {
345
- const finalUrl = prepareGETUrl({
346
- baseUrl: endpoint,
347
- query,
348
- variables: request.variables,
349
- operationName: request.operationName,
350
- extensions: request.extensions
351
- });
352
- const fetchOptions = {
353
- method: "GET",
354
- headers,
355
- signal
341
+ const query = printFn(request.document);
342
+ let serializeFn = function serialize() {
343
+ return {
344
+ query: excludeQuery ? void 0 : printFn(request.document),
345
+ variables: (request.variables && Object.keys(request.variables).length) > 0 ? request.variables : void 0,
346
+ operationName: request.operationName ? request.operationName : void 0,
347
+ extensions: request.extensions && Object.keys(request.extensions).length > 0 ? request.extensions : void 0
348
+ };
349
+ };
350
+ if (options?.apq) {
351
+ serializeFn = function serializeWithAPQ() {
352
+ return utils.mapMaybePromise(hashSHA256(query), (sha256Hash) => {
353
+ const extensions = request.extensions || {};
354
+ extensions["persistedQuery"] = {
355
+ version: 1,
356
+ sha256Hash
356
357
  };
357
- if (options?.credentials != null) {
358
- fetchOptions.credentials = options.credentials;
359
- }
360
- upstreamErrorExtensions.request.url = finalUrl;
361
- return fetchFn(finalUrl, fetchOptions, request.context, request.info);
362
- }
363
- case "POST": {
364
- const body = {
365
- query,
366
- variables: request.variables,
367
- operationName: request.operationName,
368
- extensions: request.extensions
358
+ return {
359
+ query: excludeQuery ? void 0 : query,
360
+ variables: (request.variables && Object.keys(request.variables).length) > 0 ? request.variables : void 0,
361
+ operationName: request.operationName ? request.operationName : void 0,
362
+ extensions
369
363
  };
370
- upstreamErrorExtensions.request.body = body;
371
- return utils.mapMaybePromise(
372
- createFormDataFromVariables(body, {
373
- File: options?.File,
374
- FormData: options?.FormData
375
- }),
376
- (body2) => {
377
- if (typeof body2 === "string" && !headers["content-type"]) {
378
- upstreamErrorExtensions.request.body = body2;
379
- headers["content-type"] = "application/json";
380
- }
381
- const fetchOptions = {
382
- method: "POST",
383
- body: body2,
384
- headers,
385
- signal
386
- };
387
- if (options?.credentials != null) {
388
- fetchOptions.credentials = options.credentials;
389
- }
390
- return fetchFn(
391
- endpoint,
392
- fetchOptions,
393
- request.context,
394
- request.info
395
- );
364
+ });
365
+ };
366
+ }
367
+ return utils.mapMaybePromise(
368
+ serializeFn(),
369
+ (body) => new valueOrPromise.ValueOrPromise(() => {
370
+ switch (method) {
371
+ case "GET": {
372
+ const finalUrl = prepareGETUrl({
373
+ baseUrl: endpoint,
374
+ body
375
+ });
376
+ const fetchOptions = {
377
+ method: "GET",
378
+ headers,
379
+ signal
380
+ };
381
+ if (options?.credentials != null) {
382
+ fetchOptions.credentials = options.credentials;
396
383
  }
384
+ upstreamErrorExtensions.request.url = finalUrl;
385
+ return fetchFn(
386
+ finalUrl,
387
+ fetchOptions,
388
+ request.context,
389
+ request.info
390
+ );
391
+ }
392
+ case "POST": {
393
+ upstreamErrorExtensions.request.body = body;
394
+ return utils.mapMaybePromise(
395
+ createFormDataFromVariables(body, {
396
+ File: options?.File,
397
+ FormData: options?.FormData
398
+ }),
399
+ (body2) => {
400
+ if (typeof body2 === "string" && !headers["content-type"]) {
401
+ upstreamErrorExtensions.request.body = body2;
402
+ headers["content-type"] = "application/json";
403
+ }
404
+ const fetchOptions = {
405
+ method: "POST",
406
+ body: body2,
407
+ headers,
408
+ signal
409
+ };
410
+ if (options?.credentials != null) {
411
+ fetchOptions.credentials = options.credentials;
412
+ }
413
+ return fetchFn(
414
+ endpoint,
415
+ fetchOptions,
416
+ request.context,
417
+ request.info
418
+ );
419
+ }
420
+ );
421
+ }
422
+ }
423
+ }).then((fetchResult) => {
424
+ upstreamErrorExtensions.response.status = fetchResult.status;
425
+ upstreamErrorExtensions.response.statusText = fetchResult.statusText;
426
+ Object.defineProperty(upstreamErrorExtensions.response, "headers", {
427
+ get() {
428
+ return Object.fromEntries(fetchResult.headers.entries());
429
+ }
430
+ });
431
+ if (options?.retry != null && !fetchResult.status.toString().startsWith("2")) {
432
+ throw new Error(
433
+ fetchResult.statusText || `Upstream HTTP Error: ${fetchResult.status}`
397
434
  );
398
435
  }
399
- }
400
- }).then((fetchResult) => {
401
- upstreamErrorExtensions.response.status = fetchResult.status;
402
- upstreamErrorExtensions.response.statusText = fetchResult.statusText;
403
- Object.defineProperty(upstreamErrorExtensions.response, "headers", {
404
- get() {
405
- return Object.fromEntries(fetchResult.headers.entries());
436
+ const contentType = fetchResult.headers.get("content-type");
437
+ if (contentType?.includes("text/event-stream")) {
438
+ return handleEventStreamResponse(signal, fetchResult);
439
+ } else if (contentType?.includes("multipart/mixed")) {
440
+ return handleMultipartMixedResponse(fetchResult);
406
441
  }
407
- });
408
- if (options?.retry != null && !fetchResult.status.toString().startsWith("2")) {
409
- throw new Error(
410
- fetchResult.statusText || `Upstream HTTP Error: ${fetchResult.status}`
411
- );
412
- }
413
- const contentType = fetchResult.headers.get("content-type");
414
- if (contentType?.includes("text/event-stream")) {
415
- return handleEventStreamResponse(signal, fetchResult);
416
- } else if (contentType?.includes("multipart/mixed")) {
417
- return handleMultipartMixedResponse(fetchResult);
418
- }
419
- return fetchResult.text();
420
- }).then((result) => {
421
- if (typeof result === "string") {
422
- upstreamErrorExtensions.response.body = result;
423
- if (result) {
424
- try {
425
- const parsedResult = JSON.parse(result);
426
- upstreamErrorExtensions.response.body = parsedResult;
427
- if (parsedResult.data == null && (parsedResult.errors == null || parsedResult.errors.length === 0)) {
442
+ return fetchResult.text();
443
+ }).then((result) => {
444
+ if (typeof result === "string") {
445
+ upstreamErrorExtensions.response.body = result;
446
+ if (result) {
447
+ try {
448
+ const parsedResult = JSON.parse(result);
449
+ upstreamErrorExtensions.response.body = parsedResult;
450
+ if (parsedResult.data == null && (parsedResult.errors == null || parsedResult.errors.length === 0)) {
451
+ return {
452
+ errors: [
453
+ utils.createGraphQLError(
454
+ 'Unexpected empty "data" and "errors" fields in result: ' + result,
455
+ {
456
+ extensions: upstreamErrorExtensions
457
+ }
458
+ )
459
+ ]
460
+ };
461
+ }
462
+ if (Array.isArray(parsedResult.errors)) {
463
+ return {
464
+ ...parsedResult,
465
+ errors: parsedResult.errors.map(
466
+ ({
467
+ message,
468
+ ...options2
469
+ }) => utils.createGraphQLError(message, {
470
+ ...options2,
471
+ extensions: {
472
+ code: "DOWNSTREAM_SERVICE_ERROR",
473
+ ...options2.extensions || {}
474
+ }
475
+ })
476
+ )
477
+ };
478
+ }
479
+ return parsedResult;
480
+ } catch (e) {
428
481
  return {
429
482
  errors: [
430
483
  utils.createGraphQLError(
431
- 'Unexpected empty "data" and "errors" fields in result: ' + result,
484
+ `Unexpected response: ${JSON.stringify(result)}`,
432
485
  {
433
- extensions: upstreamErrorExtensions
486
+ extensions: upstreamErrorExtensions,
487
+ originalError: e
434
488
  }
435
489
  )
436
490
  ]
437
491
  };
438
492
  }
439
- if (Array.isArray(parsedResult.errors)) {
440
- return {
441
- ...parsedResult,
442
- errors: parsedResult.errors.map(
443
- ({
444
- message,
445
- ...options2
446
- }) => utils.createGraphQLError(message, {
447
- ...options2,
448
- extensions: {
449
- code: "DOWNSTREAM_SERVICE_ERROR",
450
- ...options2.extensions || {}
451
- }
452
- })
453
- )
454
- };
455
- }
456
- return parsedResult;
457
- } catch (e) {
458
- return {
459
- errors: [
460
- utils.createGraphQLError(
461
- `Unexpected response: ${JSON.stringify(result)}`,
462
- {
463
- extensions: upstreamErrorExtensions,
464
- originalError: e
465
- }
466
- )
467
- ]
468
- };
469
493
  }
494
+ } else {
495
+ return result;
496
+ }
497
+ }).catch((e) => {
498
+ if (e.name === "AggregateError") {
499
+ return {
500
+ errors: e.errors.map(
501
+ (e2) => coerceFetchError(e2, {
502
+ signal,
503
+ endpoint,
504
+ upstreamErrorExtensions
505
+ })
506
+ )
507
+ };
470
508
  }
471
- } else {
472
- return result;
473
- }
474
- }).catch((e) => {
475
- if (e.name === "AggregateError") {
476
509
  return {
477
- errors: e.errors.map(
478
- (e2) => coerceFetchError(e2, {
510
+ errors: [
511
+ coerceFetchError(e, {
479
512
  signal,
480
513
  endpoint,
481
514
  upstreamErrorExtensions
482
515
  })
483
- )
516
+ ]
484
517
  };
485
- }
486
- return {
487
- errors: [
488
- coerceFetchError(e, {
489
- signal,
490
- endpoint,
491
- upstreamErrorExtensions
492
- })
493
- ]
494
- };
495
- }).resolve();
518
+ }).resolve()
519
+ );
496
520
  };
497
521
  let executor = baseExecutor;
522
+ if (options?.apq != null) {
523
+ executor = function apqExecutor(request) {
524
+ return utils.mapMaybePromise(
525
+ baseExecutor(request, true),
526
+ (res) => {
527
+ if (res.errors?.some(
528
+ (error) => error.extensions["code"] === "PERSISTED_QUERY_NOT_FOUND" || error.message === "PersistedQueryNotFound"
529
+ )) {
530
+ return baseExecutor(request, false);
531
+ }
532
+ return res;
533
+ }
534
+ );
535
+ };
536
+ }
498
537
  if (options?.retry != null) {
538
+ const prevExecutor = executor;
499
539
  executor = function retryExecutor(request) {
500
540
  let result;
501
541
  let attempt = 0;
@@ -512,7 +552,7 @@ function buildHTTPExecutor(options) {
512
552
  errors: [utils.createGraphQLError("No response returned from fetch")]
513
553
  };
514
554
  }
515
- return utils.mapMaybePromise(baseExecutor(request), (res) => {
555
+ return utils.mapMaybePromise(prevExecutor(request), (res) => {
516
556
  result = res;
517
557
  if (result?.errors?.length) {
518
558
  return retryAttempt();
package/dist/index.d.cts CHANGED
@@ -55,12 +55,19 @@ interface HTTPExecutorOptions {
55
55
  * Print function for DocumentNode
56
56
  */
57
57
  print?: (doc: DocumentNode) => string;
58
+ apq?: boolean;
58
59
  /**
59
60
  * Enable [Explicit Resource Management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)
60
61
  * @deprecated The executors are always disposable, and this option will be removed in the next major version, there is no need to have a flag for this.
61
62
  */
62
63
  disposable?: boolean;
63
64
  }
65
+ type SerializedRequest = {
66
+ query?: string;
67
+ variables?: Record<string, any>;
68
+ operationName?: string;
69
+ extensions?: any;
70
+ };
64
71
  type HeadersConfig = Record<string, string>;
65
72
  declare function buildHTTPExecutor(options?: Omit<HTTPExecutorOptions, 'fetch'> & {
66
73
  fetch: SyncFetchFn;
@@ -73,4 +80,4 @@ declare function buildHTTPExecutor(options?: Omit<HTTPExecutorOptions, 'fetch'>
73
80
  }): DisposableAsyncExecutor<any, HTTPExecutorOptions>;
74
81
  declare function buildHTTPExecutor(options?: Omit<HTTPExecutorOptions, 'fetch'>): DisposableAsyncExecutor<any, HTTPExecutorOptions>;
75
82
 
76
- export { type AsyncFetchFn, type AsyncImportFn, type FetchFn, type HTTPExecutorOptions, type HeadersConfig, type RegularFetchFn, type SyncFetchFn, type SyncImportFn, type SyncResponse, buildHTTPExecutor, isLiveQueryOperationDefinitionNode };
83
+ export { type AsyncFetchFn, type AsyncImportFn, type FetchFn, type HTTPExecutorOptions, type HeadersConfig, type RegularFetchFn, type SerializedRequest, type SyncFetchFn, type SyncImportFn, type SyncResponse, buildHTTPExecutor, isLiveQueryOperationDefinitionNode };
package/dist/index.d.ts CHANGED
@@ -55,12 +55,19 @@ interface HTTPExecutorOptions {
55
55
  * Print function for DocumentNode
56
56
  */
57
57
  print?: (doc: DocumentNode) => string;
58
+ apq?: boolean;
58
59
  /**
59
60
  * Enable [Explicit Resource Management](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management)
60
61
  * @deprecated The executors are always disposable, and this option will be removed in the next major version, there is no need to have a flag for this.
61
62
  */
62
63
  disposable?: boolean;
63
64
  }
65
+ type SerializedRequest = {
66
+ query?: string;
67
+ variables?: Record<string, any>;
68
+ operationName?: string;
69
+ extensions?: any;
70
+ };
64
71
  type HeadersConfig = Record<string, string>;
65
72
  declare function buildHTTPExecutor(options?: Omit<HTTPExecutorOptions, 'fetch'> & {
66
73
  fetch: SyncFetchFn;
@@ -73,4 +80,4 @@ declare function buildHTTPExecutor(options?: Omit<HTTPExecutorOptions, 'fetch'>
73
80
  }): DisposableAsyncExecutor<any, HTTPExecutorOptions>;
74
81
  declare function buildHTTPExecutor(options?: Omit<HTTPExecutorOptions, 'fetch'>): DisposableAsyncExecutor<any, HTTPExecutorOptions>;
75
82
 
76
- export { type AsyncFetchFn, type AsyncImportFn, type FetchFn, type HTTPExecutorOptions, type HeadersConfig, type RegularFetchFn, type SyncFetchFn, type SyncImportFn, type SyncResponse, buildHTTPExecutor, isLiveQueryOperationDefinitionNode };
83
+ export { type AsyncFetchFn, type AsyncImportFn, type FetchFn, type HTTPExecutorOptions, type HeadersConfig, type RegularFetchFn, type SerializedRequest, type SyncFetchFn, type SyncImportFn, type SyncResponse, buildHTTPExecutor, isLiveQueryOperationDefinitionNode };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { isAsyncIterable, isPromise, mapMaybePromise, memoize1, createGraphQLError, inspect, mapAsyncIterator, mergeIncrementalResult, getOperationASTFromRequest } from '@graphql-tools/utils';
2
2
  import { DisposableSymbols } from '@whatwg-node/disposablestack';
3
- import { File, FormData, TextDecoder, fetch } from '@whatwg-node/fetch';
3
+ import { File, FormData, TextEncoder, crypto, TextDecoder, fetch } from '@whatwg-node/fetch';
4
4
  import { ValueOrPromise } from 'value-or-promise';
5
5
  import { extractFiles, isExtractableFile } from 'extract-files';
6
6
  import { stripIgnoredCharacters, print } from 'graphql';
@@ -28,32 +28,21 @@ function collectAsyncIterableValues(asyncIterable) {
28
28
  }
29
29
  return iterate();
30
30
  }
31
- function createFormDataFromVariables({
32
- query,
33
- variables,
34
- operationName,
35
- extensions
36
- }, {
31
+ function createFormDataFromVariables(body, {
37
32
  File: FileCtor = File,
38
33
  FormData: FormDataCtor = FormData
39
34
  }) {
40
- const vars = Object.assign({}, variables);
35
+ if (!body.variables) {
36
+ return JSON.stringify(body);
37
+ }
38
+ const vars = Object.assign({}, body.variables);
41
39
  const { clone, files } = extractFiles(
42
40
  vars,
43
41
  "variables",
44
42
  (v) => isExtractableFile(v) || v?.promise || isAsyncIterable(v) || v?.then || typeof v?.arrayBuffer === "function"
45
43
  );
46
44
  if (files.size === 0) {
47
- return JSON.stringify(
48
- {
49
- query,
50
- variables,
51
- operationName,
52
- extensions
53
- },
54
- null,
55
- 2
56
- );
45
+ return JSON.stringify(body);
57
46
  }
58
47
  const map = {};
59
48
  const uploads = [];
@@ -67,10 +56,8 @@ function createFormDataFromVariables({
67
56
  form.append(
68
57
  "operations",
69
58
  JSON.stringify({
70
- query,
71
- variables: clone,
72
- operationName,
73
- extensions
59
+ ...body,
60
+ variables: clone
74
61
  })
75
62
  );
76
63
  form.append("map", JSON.stringify(map));
@@ -148,6 +135,20 @@ function createResultForAbort(reason, extensions) {
148
135
  errors: [createGraphQLErrorForAbort(reason, extensions)]
149
136
  };
150
137
  }
138
+ function hashSHA256(str) {
139
+ const textEncoder = new TextEncoder();
140
+ const utf8 = textEncoder.encode(str);
141
+ return mapMaybePromise(
142
+ crypto.subtle.digest("SHA-256", utf8),
143
+ (hashBuffer) => {
144
+ let hashHex = "";
145
+ for (const bytes of new Uint8Array(hashBuffer)) {
146
+ hashHex += bytes.toString(16).padStart(2, "0");
147
+ }
148
+ return hashHex;
149
+ }
150
+ );
151
+ }
151
152
 
152
153
  const DELIM = "\n\n";
153
154
  function isReadableStream(value) {
@@ -250,23 +251,22 @@ const isLiveQueryOperationDefinitionNode = memoize1(
250
251
 
251
252
  function prepareGETUrl({
252
253
  baseUrl = "",
253
- query,
254
- variables,
255
- operationName,
256
- extensions
254
+ body
257
255
  }) {
258
256
  const dummyHostname = "https://dummyhostname.com";
259
257
  const validUrl = baseUrl.startsWith("http") ? baseUrl : baseUrl?.startsWith("/") ? `${dummyHostname}${baseUrl}` : `${dummyHostname}/${baseUrl}`;
260
258
  const urlObj = new URL(validUrl);
261
- urlObj.searchParams.set("query", stripIgnoredCharacters(query));
262
- if (variables && Object.keys(variables).length > 0) {
263
- urlObj.searchParams.set("variables", JSON.stringify(variables));
259
+ if (body.query) {
260
+ urlObj.searchParams.set("query", body.query);
261
+ }
262
+ if (body.variables && Object.keys(body.variables).length > 0) {
263
+ urlObj.searchParams.set("variables", JSON.stringify(body.variables));
264
264
  }
265
- if (operationName) {
266
- urlObj.searchParams.set("operationName", operationName);
265
+ if (body.operationName) {
266
+ urlObj.searchParams.set("operationName", body.operationName);
267
267
  }
268
- if (extensions) {
269
- urlObj.searchParams.set("extensions", JSON.stringify(extensions));
268
+ if (body.extensions) {
269
+ urlObj.searchParams.set("extensions", JSON.stringify(body.extensions));
270
270
  }
271
271
  const finalUrl = urlObj.toString().replace(dummyHostname, "");
272
272
  return finalUrl;
@@ -292,7 +292,7 @@ function buildHTTPExecutor(options) {
292
292
  const printFn = options?.print ?? defaultPrintFn;
293
293
  const disposeCtrl = new AbortController();
294
294
  const sharedSignal = createSignalWrapper(disposeCtrl.signal);
295
- const baseExecutor = (request) => {
295
+ const baseExecutor = (request, excludeQuery) => {
296
296
  if (sharedSignal.aborted) {
297
297
  return createResultForAbort(sharedSignal.reason);
298
298
  }
@@ -323,7 +323,6 @@ function buildHTTPExecutor(options) {
323
323
  Object.assign(headers, headersFromExtensions);
324
324
  request.extensions = restExtensions;
325
325
  }
326
- const query = printFn(request.document);
327
326
  let signal = sharedSignal;
328
327
  if (options?.timeout) {
329
328
  signal = AbortSignal.any([
@@ -337,163 +336,204 @@ function buildHTTPExecutor(options) {
337
336
  },
338
337
  response: {}
339
338
  };
340
- return new ValueOrPromise(() => {
341
- switch (method) {
342
- case "GET": {
343
- const finalUrl = prepareGETUrl({
344
- baseUrl: endpoint,
345
- query,
346
- variables: request.variables,
347
- operationName: request.operationName,
348
- extensions: request.extensions
349
- });
350
- const fetchOptions = {
351
- method: "GET",
352
- headers,
353
- signal
339
+ const query = printFn(request.document);
340
+ let serializeFn = function serialize() {
341
+ return {
342
+ query: excludeQuery ? void 0 : printFn(request.document),
343
+ variables: (request.variables && Object.keys(request.variables).length) > 0 ? request.variables : void 0,
344
+ operationName: request.operationName ? request.operationName : void 0,
345
+ extensions: request.extensions && Object.keys(request.extensions).length > 0 ? request.extensions : void 0
346
+ };
347
+ };
348
+ if (options?.apq) {
349
+ serializeFn = function serializeWithAPQ() {
350
+ return mapMaybePromise(hashSHA256(query), (sha256Hash) => {
351
+ const extensions = request.extensions || {};
352
+ extensions["persistedQuery"] = {
353
+ version: 1,
354
+ sha256Hash
354
355
  };
355
- if (options?.credentials != null) {
356
- fetchOptions.credentials = options.credentials;
357
- }
358
- upstreamErrorExtensions.request.url = finalUrl;
359
- return fetchFn(finalUrl, fetchOptions, request.context, request.info);
360
- }
361
- case "POST": {
362
- const body = {
363
- query,
364
- variables: request.variables,
365
- operationName: request.operationName,
366
- extensions: request.extensions
356
+ return {
357
+ query: excludeQuery ? void 0 : query,
358
+ variables: (request.variables && Object.keys(request.variables).length) > 0 ? request.variables : void 0,
359
+ operationName: request.operationName ? request.operationName : void 0,
360
+ extensions
367
361
  };
368
- upstreamErrorExtensions.request.body = body;
369
- return mapMaybePromise(
370
- createFormDataFromVariables(body, {
371
- File: options?.File,
372
- FormData: options?.FormData
373
- }),
374
- (body2) => {
375
- if (typeof body2 === "string" && !headers["content-type"]) {
376
- upstreamErrorExtensions.request.body = body2;
377
- headers["content-type"] = "application/json";
378
- }
379
- const fetchOptions = {
380
- method: "POST",
381
- body: body2,
382
- headers,
383
- signal
384
- };
385
- if (options?.credentials != null) {
386
- fetchOptions.credentials = options.credentials;
387
- }
388
- return fetchFn(
389
- endpoint,
390
- fetchOptions,
391
- request.context,
392
- request.info
393
- );
362
+ });
363
+ };
364
+ }
365
+ return mapMaybePromise(
366
+ serializeFn(),
367
+ (body) => new ValueOrPromise(() => {
368
+ switch (method) {
369
+ case "GET": {
370
+ const finalUrl = prepareGETUrl({
371
+ baseUrl: endpoint,
372
+ body
373
+ });
374
+ const fetchOptions = {
375
+ method: "GET",
376
+ headers,
377
+ signal
378
+ };
379
+ if (options?.credentials != null) {
380
+ fetchOptions.credentials = options.credentials;
394
381
  }
382
+ upstreamErrorExtensions.request.url = finalUrl;
383
+ return fetchFn(
384
+ finalUrl,
385
+ fetchOptions,
386
+ request.context,
387
+ request.info
388
+ );
389
+ }
390
+ case "POST": {
391
+ upstreamErrorExtensions.request.body = body;
392
+ return mapMaybePromise(
393
+ createFormDataFromVariables(body, {
394
+ File: options?.File,
395
+ FormData: options?.FormData
396
+ }),
397
+ (body2) => {
398
+ if (typeof body2 === "string" && !headers["content-type"]) {
399
+ upstreamErrorExtensions.request.body = body2;
400
+ headers["content-type"] = "application/json";
401
+ }
402
+ const fetchOptions = {
403
+ method: "POST",
404
+ body: body2,
405
+ headers,
406
+ signal
407
+ };
408
+ if (options?.credentials != null) {
409
+ fetchOptions.credentials = options.credentials;
410
+ }
411
+ return fetchFn(
412
+ endpoint,
413
+ fetchOptions,
414
+ request.context,
415
+ request.info
416
+ );
417
+ }
418
+ );
419
+ }
420
+ }
421
+ }).then((fetchResult) => {
422
+ upstreamErrorExtensions.response.status = fetchResult.status;
423
+ upstreamErrorExtensions.response.statusText = fetchResult.statusText;
424
+ Object.defineProperty(upstreamErrorExtensions.response, "headers", {
425
+ get() {
426
+ return Object.fromEntries(fetchResult.headers.entries());
427
+ }
428
+ });
429
+ if (options?.retry != null && !fetchResult.status.toString().startsWith("2")) {
430
+ throw new Error(
431
+ fetchResult.statusText || `Upstream HTTP Error: ${fetchResult.status}`
395
432
  );
396
433
  }
397
- }
398
- }).then((fetchResult) => {
399
- upstreamErrorExtensions.response.status = fetchResult.status;
400
- upstreamErrorExtensions.response.statusText = fetchResult.statusText;
401
- Object.defineProperty(upstreamErrorExtensions.response, "headers", {
402
- get() {
403
- return Object.fromEntries(fetchResult.headers.entries());
434
+ const contentType = fetchResult.headers.get("content-type");
435
+ if (contentType?.includes("text/event-stream")) {
436
+ return handleEventStreamResponse(signal, fetchResult);
437
+ } else if (contentType?.includes("multipart/mixed")) {
438
+ return handleMultipartMixedResponse(fetchResult);
404
439
  }
405
- });
406
- if (options?.retry != null && !fetchResult.status.toString().startsWith("2")) {
407
- throw new Error(
408
- fetchResult.statusText || `Upstream HTTP Error: ${fetchResult.status}`
409
- );
410
- }
411
- const contentType = fetchResult.headers.get("content-type");
412
- if (contentType?.includes("text/event-stream")) {
413
- return handleEventStreamResponse(signal, fetchResult);
414
- } else if (contentType?.includes("multipart/mixed")) {
415
- return handleMultipartMixedResponse(fetchResult);
416
- }
417
- return fetchResult.text();
418
- }).then((result) => {
419
- if (typeof result === "string") {
420
- upstreamErrorExtensions.response.body = result;
421
- if (result) {
422
- try {
423
- const parsedResult = JSON.parse(result);
424
- upstreamErrorExtensions.response.body = parsedResult;
425
- if (parsedResult.data == null && (parsedResult.errors == null || parsedResult.errors.length === 0)) {
440
+ return fetchResult.text();
441
+ }).then((result) => {
442
+ if (typeof result === "string") {
443
+ upstreamErrorExtensions.response.body = result;
444
+ if (result) {
445
+ try {
446
+ const parsedResult = JSON.parse(result);
447
+ upstreamErrorExtensions.response.body = parsedResult;
448
+ if (parsedResult.data == null && (parsedResult.errors == null || parsedResult.errors.length === 0)) {
449
+ return {
450
+ errors: [
451
+ createGraphQLError(
452
+ 'Unexpected empty "data" and "errors" fields in result: ' + result,
453
+ {
454
+ extensions: upstreamErrorExtensions
455
+ }
456
+ )
457
+ ]
458
+ };
459
+ }
460
+ if (Array.isArray(parsedResult.errors)) {
461
+ return {
462
+ ...parsedResult,
463
+ errors: parsedResult.errors.map(
464
+ ({
465
+ message,
466
+ ...options2
467
+ }) => createGraphQLError(message, {
468
+ ...options2,
469
+ extensions: {
470
+ code: "DOWNSTREAM_SERVICE_ERROR",
471
+ ...options2.extensions || {}
472
+ }
473
+ })
474
+ )
475
+ };
476
+ }
477
+ return parsedResult;
478
+ } catch (e) {
426
479
  return {
427
480
  errors: [
428
481
  createGraphQLError(
429
- 'Unexpected empty "data" and "errors" fields in result: ' + result,
482
+ `Unexpected response: ${JSON.stringify(result)}`,
430
483
  {
431
- extensions: upstreamErrorExtensions
484
+ extensions: upstreamErrorExtensions,
485
+ originalError: e
432
486
  }
433
487
  )
434
488
  ]
435
489
  };
436
490
  }
437
- if (Array.isArray(parsedResult.errors)) {
438
- return {
439
- ...parsedResult,
440
- errors: parsedResult.errors.map(
441
- ({
442
- message,
443
- ...options2
444
- }) => createGraphQLError(message, {
445
- ...options2,
446
- extensions: {
447
- code: "DOWNSTREAM_SERVICE_ERROR",
448
- ...options2.extensions || {}
449
- }
450
- })
451
- )
452
- };
453
- }
454
- return parsedResult;
455
- } catch (e) {
456
- return {
457
- errors: [
458
- createGraphQLError(
459
- `Unexpected response: ${JSON.stringify(result)}`,
460
- {
461
- extensions: upstreamErrorExtensions,
462
- originalError: e
463
- }
464
- )
465
- ]
466
- };
467
491
  }
492
+ } else {
493
+ return result;
494
+ }
495
+ }).catch((e) => {
496
+ if (e.name === "AggregateError") {
497
+ return {
498
+ errors: e.errors.map(
499
+ (e2) => coerceFetchError(e2, {
500
+ signal,
501
+ endpoint,
502
+ upstreamErrorExtensions
503
+ })
504
+ )
505
+ };
468
506
  }
469
- } else {
470
- return result;
471
- }
472
- }).catch((e) => {
473
- if (e.name === "AggregateError") {
474
507
  return {
475
- errors: e.errors.map(
476
- (e2) => coerceFetchError(e2, {
508
+ errors: [
509
+ coerceFetchError(e, {
477
510
  signal,
478
511
  endpoint,
479
512
  upstreamErrorExtensions
480
513
  })
481
- )
514
+ ]
482
515
  };
483
- }
484
- return {
485
- errors: [
486
- coerceFetchError(e, {
487
- signal,
488
- endpoint,
489
- upstreamErrorExtensions
490
- })
491
- ]
492
- };
493
- }).resolve();
516
+ }).resolve()
517
+ );
494
518
  };
495
519
  let executor = baseExecutor;
520
+ if (options?.apq != null) {
521
+ executor = function apqExecutor(request) {
522
+ return mapMaybePromise(
523
+ baseExecutor(request, true),
524
+ (res) => {
525
+ if (res.errors?.some(
526
+ (error) => error.extensions["code"] === "PERSISTED_QUERY_NOT_FOUND" || error.message === "PersistedQueryNotFound"
527
+ )) {
528
+ return baseExecutor(request, false);
529
+ }
530
+ return res;
531
+ }
532
+ );
533
+ };
534
+ }
496
535
  if (options?.retry != null) {
536
+ const prevExecutor = executor;
497
537
  executor = function retryExecutor(request) {
498
538
  let result;
499
539
  let attempt = 0;
@@ -510,7 +550,7 @@ function buildHTTPExecutor(options) {
510
550
  errors: [createGraphQLError("No response returned from fetch")]
511
551
  };
512
552
  }
513
- return mapMaybePromise(baseExecutor(request), (res) => {
553
+ return mapMaybePromise(prevExecutor(request), (res) => {
514
554
  result = res;
515
555
  if (result?.errors?.length) {
516
556
  return retryAttempt();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphql-tools/executor-http",
3
- "version": "1.1.14",
3
+ "version": "1.2.0-alpha-753096403f92c5dbcdd17828acdd17a1aa53a51e",
4
4
  "type": "module",
5
5
  "description": "A set of utils for faster development of GraphQL tools",
6
6
  "repository": {
@@ -49,6 +49,7 @@
49
49
  "value-or-promise": "^1.0.12"
50
50
  },
51
51
  "devDependencies": {
52
+ "@apollo/server": "^4.11.2",
52
53
  "@types/extract-files": "8.1.3",
53
54
  "@whatwg-node/disposablestack": "^0.0.5",
54
55
  "graphql": "^16.9.0",