diff --git a/changelog.md b/changelog.md index 8448e20..6b07b3a 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,10 @@ - Use the TypeScript v5.5+ JSDoc tag `@import` to import types in modules. - Updated dev dependencies, some of which require newer Node.js versions than previously supported. +### Minor + +- Added a new function `createUploadLink` option `includeUnusedVariables` defaulting to `false` to toggle including unused GraphQL variables in the request (similar to the Apollo `BaseHttpLink` option [`includeUnusedVariables`](https://www.apollographql.com/docs/react/api/link/apollo-link-base-http#options-includeunusedvariables)), via [#348](https://github.com/jaydenseric/apollo-upload-client/pull/348). + ### Patch - Updated the package scripts: diff --git a/createUploadLink.mjs b/createUploadLink.mjs index 2fffe7c..f72b0fd 100644 --- a/createUploadLink.mjs +++ b/createUploadLink.mjs @@ -18,6 +18,7 @@ import { } from "@apollo/client/link/http/selectHttpOptionsAndBody.js"; import { selectURI } from "@apollo/client/link/http/selectURI.js"; import { serializeFetchParameter } from "@apollo/client/link/http/serializeFetchParameter.js"; +import { filterOperationVariables } from "@apollo/client/link/utils/filterOperationVariables.js"; import { Observable } from "@apollo/client/utilities/observables/Observable.js"; import extractFiles from "extract-files/extractFiles.mjs"; @@ -69,6 +70,8 @@ import isExtractableFile from "./isExtractableFile.mjs"; * {@linkcode fetchOptions}. * @param {boolean} [options.includeExtensions] Toggles sending `extensions` * fields to the GraphQL server. Defaults to `false`. + * @param {boolean} [options.includeUnusedVariables] Toggles including unused + * GraphQL variables in the request. Defaults to `false`. * @returns A [terminating Apollo Link](https://www.apollographql.com/docs/react/api/link/introduction/#the-terminating-link). * @example * A basic Apollo Client setup: @@ -95,6 +98,7 @@ export default function createUploadLink({ credentials, headers, includeExtensions, + includeUnusedVariables = false, } = {}) { const linkConfig = { http: { includeExtensions }, @@ -142,6 +146,12 @@ export default function createUploadLink({ contextConfig, ); + if (body.variables && !includeUnusedVariables) + body.variables = filterOperationVariables( + body.variables, + operation.query, + ); + const { clone, files } = extractFiles(body, customIsExtractableFile, ""); let uri = selectURI(operation, fetchUri); diff --git a/createUploadLink.test.mjs b/createUploadLink.test.mjs index d16daee..df415a7 100644 --- a/createUploadLink.test.mjs +++ b/createUploadLink.test.mjs @@ -314,6 +314,147 @@ describe("Function `createUploadLink`.", { concurrency: true }, () => { deepStrictEqual(nextData, payload); }); + it("Option `includeUnusedVariables`, set to false.", async () => { + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ + let fetchOptions; + + /** @type {unknown} */ + let nextData; + + const query = "query ($a: Boolean) {\n a(a: $a)\n}"; + const payload = { data: { a: true } }; + + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + includeUnusedVariables: false, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), + variables: { + a: true, + b: true, + }, + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); + + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); + + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ + variables: { + a: true, + }, + query, + }), + }); + deepStrictEqual(nextData, payload); + }); + + it("Option `includeUnusedVariables`, set to true.", async () => { + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ + let fetchOptions; + + /** @type {unknown} */ + let nextData; + + const query = "query ($a: Boolean) {\n a(a: $a)\n}"; + const payload = { data: { a: true } }; + + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + includeUnusedVariables: true, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), + variables: { + a: true, + b: true, + }, + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); + + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); + + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ + variables: { + a: true, + b: true, + }, + query, + }), + }); + deepStrictEqual(nextData, payload); + }); + it("Option `print`.", async () => { /** @type {unknown} */ let fetchInput; @@ -606,6 +747,7 @@ describe("Function `createUploadLink`.", { concurrency: true }, () => { execute( createUploadLink({ useGETForQueries: true, + includeUnusedVariables: true, async fetch() { fetched = true;