diff --git a/README.md b/README.md index c296fe1ac..999a3fc70 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,6 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps - [Cookie support for `node`](#cookie-support-for-node) - [Using a custom `fetch` method](#using-a-custom-fetch-method) - [Receiving a raw response](#receiving-a-raw-response) - - [File Upload](#file-upload) - - [Browser](#browser) - - [Node](#node) - [Batching](#batching) - [Cancellation](#cancellation) - [Middleware](#middleware) @@ -39,6 +36,7 @@ Minimal GraphQL client supporting Node and browsers for scripts or simple apps - [Ignore](#ignore) - [All](#all) - [Knowledge Base](#knowledge-base) + - [Why was the file upload feature taken away? Will it return?](#why-was-the-file-upload-feature-taken-away-will-it-return) - [Why do I have to install `graphql`?](#why-do-i-have-to-install-graphql) - [Do I need to wrap my GraphQL documents inside the `gql` template exported by `graphql-request`?](#do-i-need-to-wrap-my-graphql-documents-inside-the-gql-template-exported-by-graphql-request) - [What's the difference between `graphql-request`, Apollo and Relay?](#whats-the-difference-between-graphql-request-apollo-and-relay) @@ -565,45 +563,6 @@ async function main() { main().catch((error) => console.error(error)) ``` -### File Upload - -#### Browser - -```js -import { request } from 'graphql-request' - -const UploadUserAvatar = gql` - mutation uploadUserAvatar($userId: Int!, $file: Upload!) { - updateUser(id: $userId, input: { avatar: $file }) - } -` - -request('/api/graphql', UploadUserAvatar, { - userId: 1, - file: document.querySelector('input#avatar').files[0], -}) -``` - -#### Node - -```js -import { createReadStream } from 'fs' -import { request } from 'graphql-request' - -const UploadUserAvatar = gql` - mutation uploadUserAvatar($userId: Int!, $file: Upload!) { - updateUser(id: $userId, input: { avatar: $file }) - } -` - -request('/api/graphql', UploadUserAvatar, { - userId: 1, - file: createReadStream('./avatar.img'), -}) -``` - -[TypeScript Source](examples/receiving-a-raw-response.ts) - ### Batching It is possible with `graphql-request` to use [batching](https://github.com/graphql/graphql-over-http/blob/main/rfcs/Batching.md) via the `batchRequests()` function. Example available at [examples/batching-requests.ts](examples/batching-requests.ts) @@ -745,6 +704,10 @@ Return both the errors and data, only works with `rawRequest`. ## Knowledge Base +#### Why was the file upload feature taken away? Will it return? + +In [this issue](https://github.com/jasonkuhrt/graphql-request/issues/500) we decided to make this library more stable and maintainable. In principal the feature is still in scope of this library and will make a return when we find time to do the feature right. + #### Why do I have to install `graphql`? `graphql-request` uses methods exposed by the `graphql` package to handle some internal logic. On top of that, for TypeScript users, some types are used from the `graphql` package to provide better typings. diff --git a/package.json b/package.json index f27f9733a..d2bab4d82 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,7 @@ }, "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5", - "extract-files": "^9.0.0", - "form-data": "^3.0.0" + "cross-fetch": "^3.1.5" }, "peerDependencies": { "graphql": "14 - 16" @@ -75,7 +73,6 @@ "@tsconfig/node16": "^1.0.3", "@types/body-parser": "^1.19.2", "@types/express": "^4.17.17", - "@types/extract-files": "^8.1.1", "@types/node": "^18.15.11", "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^5.57.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e9501022..3d3955c41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,12 +7,6 @@ dependencies: cross-fetch: specifier: ^3.1.5 version: 3.1.5 - extract-files: - specifier: ^9.0.0 - version: 9.0.0 - form-data: - specifier: ^3.0.0 - version: 3.0.1 devDependencies: '@graphql-tools/schema': @@ -30,9 +24,6 @@ devDependencies: '@types/express': specifier: ^4.17.17 version: 4.17.17 - '@types/extract-files': - specifier: ^8.1.1 - version: 8.1.1 '@types/node': specifier: ^18.15.11 version: 18.15.11 @@ -991,10 +982,6 @@ packages: '@types/serve-static': 1.15.1 dev: true - /@types/extract-files@8.1.1: - resolution: {integrity: sha512-dMJJqBqyhsfJKuK7p7HyyNmki7qj1AlwhUKWx6KrU7i1K2T2SPsUsSUTWFmr/sEM1q8rfR8j5IyUmYrDbrhfjQ==} - dev: true - /@types/istanbul-lib-coverage@2.0.4: resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} dev: true @@ -1502,6 +1489,7 @@ packages: /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true /aws-sign2@0.7.0: resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} @@ -1731,6 +1719,7 @@ packages: engines: {node: '>= 0.8'} dependencies: delayed-stream: 1.0.0 + dev: true /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -1872,6 +1861,7 @@ packages: /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dev: true /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} @@ -2303,11 +2293,6 @@ packages: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} dev: true - /extract-files@9.0.0: - resolution: {integrity: sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==} - engines: {node: ^10.17.0 || ^12.0.0 || >= 13.7.0} - dev: false - /extsprintf@1.3.0: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} @@ -2425,15 +2410,6 @@ packages: mime-types: 2.1.35 dev: true - /form-data@3.0.1: - resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} - engines: {node: '>= 6'} - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - mime-types: 2.1.35 - dev: false - /format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -3238,12 +3214,14 @@ packages: /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + dev: true /mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} dependencies: mime-db: 1.52.0 + dev: true /mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} diff --git a/src/createRequestBody.ts b/src/createRequestBody.ts deleted file mode 100644 index 7aed9f8fe..000000000 --- a/src/createRequestBody.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { defaultJsonSerializer } from './defaultJsonSerializer.js' -import type { JsonSerializer, Variables } from './types.js' -import type { ExtractableFile } from 'extract-files' -import { extractFiles, isExtractableFile } from 'extract-files' -import FormDataNode from 'form-data' - -/** - * Duck type if NodeJS stream - * https://github.com/sindresorhus/is-stream/blob/3750505b0727f6df54324784fe369365ef78841e/index.js#L3 - */ -const isExtractableFileEnhanced = (value: unknown): value is ExtractableFile | { pipe: () => unknown } => - isExtractableFile(value) || - (typeof value === `object` && value !== null && `pipe` in value && typeof value.pipe === `function`) - -/** - * Returns Multipart Form if body contains files - * (https://github.com/jaydenseric/graphql-multipart-request-spec) - * Otherwise returns JSON - */ -const createRequestBody = ( - query: string | string[], - variables?: Variables | Variables[], - operationName?: string, - jsonSerializer?: JsonSerializer -): string | FormData => { - const jsonSerializer_ = jsonSerializer ?? defaultJsonSerializer - // eslint-disable-next-line - const { clone, files } = extractFiles({ query, variables, operationName }, ``, isExtractableFileEnhanced) - - if (files.size === 0) { - if (!Array.isArray(query)) { - return jsonSerializer_.stringify(clone) - } - - if (typeof variables !== `undefined` && !Array.isArray(variables)) { - throw new Error(`Cannot create request body with given variable type, array expected`) - } - - // Batch support - const payload = query.reduce<{ query: string; variables: Variables | undefined }[]>( - (acc, currentQuery, index) => { - acc.push({ query: currentQuery, variables: variables ? variables[index] : undefined }) - return acc - }, - [] - ) - - return jsonSerializer_.stringify(payload) - } - - const Form = typeof FormData === `undefined` ? FormDataNode : FormData - - const form = new Form() - - form.append(`operations`, jsonSerializer_.stringify(clone)) - - const map: { [key: number]: string[] } = {} - let i = 0 - files.forEach((paths) => { - map[++i] = paths - }) - form.append(`map`, jsonSerializer_.stringify(map)) - - i = 0 - files.forEach((paths, file) => { - form.append(`${++i}`, file as any) - }) - - return form as FormData -} - -export default createRequestBody diff --git a/src/index.ts b/src/index.ts index f3e704eeb..d2f449d85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import createRequestBody from './createRequestBody.js' import { defaultJsonSerializer } from './defaultJsonSerializer.js' import { HeadersInstanceToPlainObject, uppercase } from './helpers.js' import { @@ -597,7 +596,32 @@ const parseBatchRequestsArgsExtended = (args: BatchRequestsArgs): BatchRequestsE } } -export default request +const createRequestBody = ( + query: string | string[], + variables?: Variables | Variables[], + operationName?: string, + jsonSerializer?: JsonSerializer +): string => { + const jsonSerializer_ = jsonSerializer ?? defaultJsonSerializer + if (!Array.isArray(query)) { + return jsonSerializer_.stringify({ query, variables, operationName }) + } + + if (typeof variables !== `undefined` && !Array.isArray(variables)) { + throw new Error(`Cannot create request body with given variable type, array expected`) + } + + // Batch support + const payload = query.reduce<{ query: string; variables: Variables | undefined }[]>( + (acc, currentQuery, index) => { + acc.push({ query: currentQuery, variables: variables ? variables[index] : undefined }) + return acc + }, + [] + ) + + return jsonSerializer_.stringify(payload) +} const getResult = async ( response: Response, @@ -655,3 +679,4 @@ export const gql = (chunks: TemplateStringsArray, ...variables: unknown[]): stri export { GraphQLWebSocketClient } from './graphql-ws.js' export { resolveRequestDocument } from './resolveRequestDocument.js' +export default request diff --git a/tests/json-serializer.test.ts b/tests/json-serializer.test.ts index 375bc0692..e583bef08 100644 --- a/tests/json-serializer.test.ts +++ b/tests/json-serializer.test.ts @@ -2,8 +2,6 @@ import { GraphQLClient } from '../src/index.js' import type { Fetch, Variables } from '../src/types.js' import { setupMockServer } from './__helpers.js' import { Headers, Response } from 'cross-fetch' -import { createReadStream } from 'fs' -import { join } from 'path' import { beforeEach, describe, expect, test, vitest } from 'vitest' const ctx = setupMockServer() @@ -25,69 +23,57 @@ const createMockFetch = (): Fetch => () => { return Promise.resolve(response) } -describe(`jsonSerializer option`, () => { - test(`is used for parsing response body`, async () => { - const client = new GraphQLClient(ctx.url, { - jsonSerializer: createMockSerializer(), - fetch: createMockFetch(), - }) - const result = await client.request(`{ test { name } }`) - expect(result).toEqual(testData.data) - expect(client.requestConfig.jsonSerializer?.parse).toBeCalledTimes(1) +test(`is used for parsing response body`, async () => { + const client = new GraphQLClient(ctx.url, { + jsonSerializer: createMockSerializer(), + fetch: createMockFetch(), }) + const result = await client.request(`{ test { name } }`) + expect(result).toEqual(testData.data) + expect(client.requestConfig.jsonSerializer?.parse).toBeCalledTimes(1) +}) - describe(`is used for serializing variables`, () => { - const document = `query getTest($name: String!) { test(name: $name) { name } }` - const simpleVariable = { name: `test` } - let client: GraphQLClient +describe(`is used for serializing variables`, () => { + const document = `query getTest($name: String!) { test(name: $name) { name } }` + const simpleVariable = { name: `test` } + let client: GraphQLClient - const testSingleQuery = - (expectedNumStringifyCalls = 1, variables: any = simpleVariable) => - async () => { - await client.request(document, variables) - expect(client.requestConfig.jsonSerializer?.stringify).toBeCalledTimes(expectedNumStringifyCalls) - } + const testSingleQuery = + (expectedNumStringifyCalls?: number, variables: Variables = simpleVariable) => + async () => { + await client.request(document, variables) + expect(client.requestConfig.jsonSerializer?.stringify).toBeCalledTimes(expectedNumStringifyCalls ?? 1) + } - const testBatchQuery = - (expectedNumStringifyCalls: number, variables: Variables = simpleVariable) => - async () => { - await client.batchRequests([{ document, variables }]) - expect(client.requestConfig.jsonSerializer?.stringify).toBeCalledTimes(expectedNumStringifyCalls) - } + const testBatchQuery = + (expectedNumStringifyCalls?: number, variables: Variables = simpleVariable) => + async () => { + await client.batchRequests([{ document, variables }]) + expect(client.requestConfig.jsonSerializer?.stringify).toBeCalledTimes(expectedNumStringifyCalls ?? 1) + } - describe(`request body`, () => { - beforeEach(() => { - client = new GraphQLClient(ctx.url, { - jsonSerializer: createMockSerializer(), - fetch: createMockFetch(), - }) - }) - - describe(`without files`, () => { - test(`single query`, testSingleQuery()) - test(`batch query`, testBatchQuery(1)) + describe(`request body`, () => { + beforeEach(() => { + client = new GraphQLClient(ctx.url, { + jsonSerializer: createMockSerializer(), + fetch: createMockFetch(), }) + }) - describe(`with files`, () => { - const fileName = `signal.test.ts` - const file = createReadStream(join(__dirname, fileName)) + test(`single query`, testSingleQuery()) + test(`batch query`, testBatchQuery(1)) + }) - test(`single query`, testSingleQuery(2, { ...simpleVariable, file })) - test(`batch query`, testBatchQuery(2, { ...simpleVariable, file })) + describe(`query string`, () => { + beforeEach(() => { + client = new GraphQLClient(ctx.url, { + jsonSerializer: createMockSerializer(), + fetch: createMockFetch(), + method: `GET`, }) }) - describe(`query string`, () => { - beforeEach(() => { - client = new GraphQLClient(ctx.url, { - jsonSerializer: createMockSerializer(), - fetch: createMockFetch(), - method: `GET`, - }) - }) - - test(`single query`, testSingleQuery()) - test(`batch query`, testBatchQuery(2)) // once for variable and once for query batch array - }) + test(`single query`, testSingleQuery()) + test(`batch query`, testBatchQuery(2)) // once for variable and once for query batch array }) })