From 0cd2e90c328a9c24079877fbb05e1d7f8c5e2e50 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 21 Jun 2024 23:38:36 -0400 Subject: [PATCH 01/11] feat: file upload extension --- src/layers/5_core/core.ts | 45 ++++++++++++++++--- .../5_createExtension/createExtension.ts | 9 ++++ src/layers/6_extensions/upload.ts | 39 ++++++++++++++++ src/lib/graphqlHTTP.ts | 7 +++ 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/layers/5_createExtension/createExtension.ts create mode 100644 src/layers/6_extensions/upload.ts diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index ca5e6a870..1d1aacb8f 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -2,6 +2,7 @@ import type { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql' import { print } from 'graphql' import { Anyware } from '../../lib/anyware/__.js' import { type StandardScalarVariables } from '../../lib/graphql.js' +import type { ExecutionInput } from '../../lib/graphqlHTTP.js' import { parseExecutionResult } from '../../lib/graphqlHTTP.js' import { CONTENT_TYPE_GQL } from '../../lib/http.js' import { casesExhausted } from '../../lib/prelude.js' @@ -65,10 +66,35 @@ export type HookInputPack = & InterfaceInput & TransportInput<{ url: string | URL; headers?: HeadersInit }, { schema: GraphQLSchema }> +type RequestInput<$Body> = { + url: string | URL + method: + | 'get' + | 'post' + | 'put' + | 'delete' + | 'patch' + | 'head' + | 'options' + | 'trace' + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'PATCH' + | 'HEAD' + | 'OPTIONS' + | 'TRACE' + headers?: HeadersInit + body: $Body +} + export type ExchangeInputHook = & InterfaceInput & TransportInput< - { request: Request }, + { + request: RequestInput + }, { schema: GraphQLSchema document: string | DocumentNode @@ -162,16 +188,15 @@ export const anyware = Anyware.create({ operationName: input.operationName, } - const bodyEncoded = JSON.stringify(body) - - const requestConfig = new Request(input.url, { + const requestConfig: RequestInput = { + url: input.url, method: `POST`, headers: new Headers({ 'accept': CONTENT_TYPE_GQL, ...Object.fromEntries(new Headers(input.headers).entries()), }), - body: bodyEncoded, - }) + body, + } return { ...input, @@ -190,7 +215,13 @@ export const anyware = Anyware.create({ exchange: async (input) => { switch (input.transport) { case `http`: { - const response = await fetch(input.request) + const response = await fetch( + new Request(input.request.url, { + method: input.request.method, + headers: input.request.headers, + body: JSON.stringify(input.request.body), + }), + ) return { ...input, response, diff --git a/src/layers/5_createExtension/createExtension.ts b/src/layers/5_createExtension/createExtension.ts new file mode 100644 index 000000000..c1b809c0f --- /dev/null +++ b/src/layers/5_createExtension/createExtension.ts @@ -0,0 +1,9 @@ +import type { Anyware } from '../../lib/anyware/__.js' +import type { Core } from '../5_core/__.js' + +type Extension = { + name: string + anyware?: Anyware.Extension2 +} + +export const createExtension = (input: Extension) => input diff --git a/src/layers/6_extensions/upload.ts b/src/layers/6_extensions/upload.ts new file mode 100644 index 000000000..82b432e97 --- /dev/null +++ b/src/layers/6_extensions/upload.ts @@ -0,0 +1,39 @@ +import type { StandardScalarVariables } from '../../lib/graphql.js' +import type { ExecutionInput } from '../../lib/graphqlHTTP.js' +import { createExtension } from '../5_createExtension/createExtension.js' + +export const Upload = createExtension({ + name: `Upload`, + anyware: async ({ pack }) => { + if (!isUsingUploadScalar(pack.input.variables)) return pack() + + const { exchange } = pack() + + // TODO we can probably get file upload working for in-memory schemas too :) + if (exchange.input.transport !== `http`) throw new Error(`Must use http transport for uploads.`) + + const headers = new Headers(exchange.input.request.headers) + headers.append(`content-type`, `multipart/form-data`) + + return exchange({ + ...exchange.input, + request: { + ...exchange.input.request, + body: createUploadBody({ + query: exchange.input.request.body.query, + variables: exchange.input.request.body.variables, + }), + headers, + }, + }) + }, +}) + +/** @see https://github.com/lynxtaa/awesome-graphql-client/blob/a3ecec2fdd3b72b005268a24b2364dd647dc6efd/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts#L79 */ +const createUploadBody = (input: ExecutionInput): ExecutionInput => { + return `todo` +} + +const isUsingUploadScalar = (variables: StandardScalarVariables) => { + return `todo` +} diff --git a/src/lib/graphqlHTTP.ts b/src/lib/graphqlHTTP.ts index e301319dd..b10661962 100644 --- a/src/lib/graphqlHTTP.ts +++ b/src/lib/graphqlHTTP.ts @@ -1,7 +1,14 @@ import type { GraphQLFormattedError } from 'graphql' import { type ExecutionResult, GraphQLError } from 'graphql' +import type { StandardScalarVariables } from './graphql.js' import { isPlainObject } from './prelude.js' +export type ExecutionInput = { + query: string + variables: StandardScalarVariables + operationName?: string +} + export const parseExecutionResult = (result: unknown): ExecutionResult => { if (typeof result !== `object` || result === null) { throw new Error(`Invalid execution result: result is not object`) From b849dec03d2c903f8a3d999fa4d1e9c9e7f96154 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sun, 23 Jun 2024 15:30:01 -0400 Subject: [PATCH 02/11] work --- package.json | 2 + pnpm-lock.yaml | 37 +++ src/layers/5_client/client.document.test.ts | 2 +- src/layers/5_client/client.extend.test.ts | 4 +- src/layers/5_client/client.ts | 24 +- src/layers/5_core/core.ts | 254 ++++++++++-------- .../5_createExtension/createExtension.ts | 8 +- src/layers/6_extensions/Upload/Upload.spec.ts | 51 ++++ src/layers/6_extensions/Upload/Upload.ts | 63 +++++ .../6_extensions/Upload/extractFiles.ts | 198 ++++++++++++++ src/layers/6_extensions/upload.ts | 39 --- src/lib/anyware/main.ts | 92 +++++-- tests/_/schemaUpload/schema.ts | 36 +++ tests/_/text.txt | 1 + 14 files changed, 614 insertions(+), 197 deletions(-) create mode 100644 src/layers/6_extensions/Upload/Upload.spec.ts create mode 100644 src/layers/6_extensions/Upload/Upload.ts create mode 100644 src/layers/6_extensions/Upload/extractFiles.ts delete mode 100644 src/layers/6_extensions/upload.ts create mode 100644 tests/_/schemaUpload/schema.ts create mode 100644 tests/_/text.txt diff --git a/package.json b/package.json index b0e983252..1e86d2528 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "@molt/command": "^0.9.0", + "is-plain-obj": "^4.1.0", "zod": "^3.23.8" }, "peerDependencies": { @@ -130,6 +131,7 @@ "graphql": "^16.9.0", "graphql-scalars": "^1.23.0", "graphql-tag": "^2.12.6", + "graphql-upload-minimal": "^1.6.1", "jsdom": "^24.1.0", "json-bigint": "^1.0.0", "publint": "^0.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c85c5c149..17ab3d095 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: dprint: specifier: ^0.46.2 version: 0.46.2 + is-plain-obj: + specifier: ^4.1.0 + version: 4.1.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -111,6 +114,9 @@ importers: graphql-tag: specifier: ^2.12.6 version: 2.12.6(graphql@16.9.0) + graphql-upload-minimal: + specifier: ^1.6.1 + version: 1.6.1(graphql@16.9.0) jsdom: specifier: ^24.1.0 version: 24.1.0 @@ -1147,6 +1153,10 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1897,6 +1907,12 @@ packages: peerDependencies: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-upload-minimal@1.6.1: + resolution: {integrity: sha512-wNUf/KqA0B/OguL1k6qWa4AmAduLUAhXzovh9i14SKbpBa8HX2vc7f+fR67S0rG7fSpGdM/aivxzC329/+9xXw==} + engines: {node: '>=12'} + peerDependencies: + graphql: 0.13.1 - 16 + graphql@16.9.0: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -2103,6 +2119,10 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2936,6 +2956,10 @@ packages: std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-length@6.0.0: resolution: {integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==} engines: {node: '>=16'} @@ -4581,6 +4605,10 @@ snapshots: dependencies: fill-range: 7.1.1 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -5511,6 +5539,11 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 + graphql-upload-minimal@1.6.1(graphql@16.9.0): + dependencies: + busboy: 1.6.0 + graphql: 16.9.0 + graphql@16.9.0: {} happy-dom@14.12.3: @@ -5697,6 +5730,8 @@ snapshots: is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -6626,6 +6661,8 @@ snapshots: std-env@3.7.0: {} + streamsearch@1.1.0: {} + string-length@6.0.0: dependencies: strip-ansi: 7.1.0 diff --git a/src/layers/5_client/client.document.test.ts b/src/layers/5_client/client.document.test.ts index bed8bea6d..a86ddb7ea 100644 --- a/src/layers/5_client/client.document.test.ts +++ b/src/layers/5_client/client.document.test.ts @@ -15,7 +15,7 @@ describe(`document with two queries`, () => { // todo allow sync extensions // eslint-disable-next-line - graffle.extend(async ({ exchange }) => { + graffle.use(async ({ exchange }) => { if (exchange.input.transport !== `http`) return exchange() // @ts-expect-error Nextjs exchange.input.request.next = { revalidate: 60, tags: [`menu`] } diff --git a/src/layers/5_client/client.extend.test.ts b/src/layers/5_client/client.extend.test.ts index 41dec82de..77cb3aba5 100644 --- a/src/layers/5_client/client.extend.test.ts +++ b/src/layers/5_client/client.extend.test.ts @@ -16,7 +16,7 @@ describe(`entrypoint request`, () => { expect(input.headers.get('x-foo')).toEqual(headers['x-foo']) return createResponse({ data: { id: db.id } }) }) - const client2 = client.extend(async ({ pack }) => { + const client2 = client.use(async ({ pack }) => { // todo should be raw input types but rather resolved // todo should be URL instance? // todo these input type tests should be moved down to Anyware @@ -30,7 +30,7 @@ describe(`entrypoint request`, () => { fetch.mockImplementationOnce(async () => { return createResponse({ data: { id: db.id } }) }) - const client2 = client.extend(async ({ pack }) => { + const client2 = client.use(async ({ pack }) => { const { exchange } = await pack({ ...pack.input, headers }) return await exchange(exchange.input) }) diff --git a/src/layers/5_client/client.ts b/src/layers/5_client/client.ts index 6ba7ca985..64c916258 100644 --- a/src/layers/5_client/client.ts +++ b/src/layers/5_client/client.ts @@ -11,7 +11,7 @@ import { readMaybeThunk } from '../1_Schema/core/helpers.js' import type { GlobalRegistry } from '../2_generator/globalRegistry.js' import type { DocumentObject, GraphQLObjectSelection } from '../3_SelectionSet/encode.js' import { Core } from '../5_core/__.js' -import { type HookInputEncode } from '../5_core/core.js' +import { type HookDefEncode } from '../5_core/core.js' import type { InterfaceRaw } from '../5_core/types.js' import type { ApplyInputDefaults, @@ -40,7 +40,7 @@ export type SelectionSetOrArgs = object export interface Context { retry: undefined | Anyware.Extension2 - extensions: Anyware.Extension2[] + extensions: Extension[] config: Config } @@ -56,6 +56,11 @@ export type ClientRaw<_$Config extends Config> = { rawOrThrow: (input: Omit) => Promise } +export type Extension = { + name: string + anyware?: Anyware.Extension2 +} + // dprint-ignore export type Client<$Index extends Schema.Index | null, $Config extends Config> = & ClientRaw<$Config> @@ -65,7 +70,7 @@ export type Client<$Index extends Schema.Index | null, $Config extends Config> = : {} // eslint-disable-line ) & { - extend: (extension: Anyware.Extension2) => Client<$Index, $Config> + use: (extension: Extension) => Client<$Index, $Config> retry: (extension: Anyware.Extension2) => Client<$Index, $Config> } @@ -153,7 +158,7 @@ export const create: Create = ( interface CreateState { retry?: Anyware.Extension2 - extensions: Anyware.Extension2[] + extensions: Extension[] } export const createInternal = ( @@ -191,7 +196,7 @@ export const createInternal = ( interface: interface_, schemaIndex: context.schemaIndex, }, - } as HookInputEncode + } as HookDefEncode['input'] return await run(context, initialInput) } @@ -261,11 +266,11 @@ export const createInternal = ( }, } - const run = async (context: Context, initialInput: HookInputEncode) => { + const run = async (context: Context, initialInput: HookDefEncode['input']) => { const result = await Core.anyware.run({ initialInput, retryingExtension: context.retry, - extensions: context.extensions, + extensions: context.extensions.filter(_ => _.anyware !== undefined).map(_ => _.anyware!), }) as GraffleExecutionResult return handleReturn(context, result) } @@ -281,7 +286,8 @@ export const createInternal = ( context: { config: context.config, }, - } as HookInputEncode + variables: rawInput.variables, + } as HookDefEncode['input'] return await run(context, initialInput) } @@ -297,7 +303,7 @@ export const createInternal = ( const contextWithReturnModeSet = updateContextConfig(context, { returnMode: `graphqlSuccess` }) return await runRaw(contextWithReturnModeSet, rawInput) }, - extend: (extension: Anyware.Extension2) => { + use: (extension: Extension) => { // todo test that adding extensions returns a copy of client return createInternal(input, { extensions: [...state.extensions, extension] }) }, diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index 1d1aacb8f..8aedd3336 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -2,7 +2,6 @@ import type { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql' import { print } from 'graphql' import { Anyware } from '../../lib/anyware/__.js' import { type StandardScalarVariables } from '../../lib/graphql.js' -import type { ExecutionInput } from '../../lib/graphqlHTTP.js' import { parseExecutionResult } from '../../lib/graphqlHTTP.js' import { CONTENT_TYPE_GQL } from '../../lib/http.js' import { casesExhausted } from '../../lib/prelude.js' @@ -53,20 +52,35 @@ export const hookNamesOrderedBySequence = [`encode`, `pack`, `exchange`, `unpack export type HookSequence = typeof hookNamesOrderedBySequence -export type HookInputEncode = - & InterfaceInput<{ selection: GraphQLObjectSelection }, { document: string | DocumentNode }> - & TransportInput<{ schema: string | URL }, { schema: GraphQLSchema }> - -export type HookInputPack = - & { - document: string | DocumentNode - variables: StandardScalarVariables - operationName?: string +export type HookDefEncode = { + input: + & InterfaceInput< + { selection: GraphQLObjectSelection }, + { document: string | DocumentNode; variables?: StandardScalarVariables } + > + & TransportInput<{ schema: string | URL }, { schema: GraphQLSchema }> + slots: { + body: ( + input: { query: string; variables?: StandardScalarVariables; operationName?: string }, + ) => BodyInit } - & InterfaceInput - & TransportInput<{ url: string | URL; headers?: HeadersInit }, { schema: GraphQLSchema }> +} + +export type HookDefPack = { + input: + & InterfaceInput + & TransportInput< + { url: string | URL; headers?: HeadersInit; body: BodyInit }, + { + schema: GraphQLSchema + query: string + variables?: StandardScalarVariables + operationName?: string + } + > +} -type RequestInput<$Body> = { +type RequestInput = { url: string | URL method: | 'get' @@ -86,140 +100,144 @@ type RequestInput<$Body> = { | 'OPTIONS' | 'TRACE' headers?: HeadersInit - body: $Body + body: BodyInit } -export type ExchangeInputHook = - & InterfaceInput - & TransportInput< - { - request: RequestInput - }, - { - schema: GraphQLSchema - document: string | DocumentNode - variables: StandardScalarVariables - operationName?: string - } - > +export type HookDefExchange = { + input: + & InterfaceInput + & TransportInput< + { + request: RequestInput + }, + { + schema: GraphQLSchema + query: string | DocumentNode + variables?: StandardScalarVariables + operationName?: string + } + > +} -export type HookInputUnpack = - & InterfaceInput - & TransportInput< - { response: Response }, - { - result: ExecutionResult - } - > +export type HookDefUnpack = { + input: + & InterfaceInput + & TransportInput< + { response: Response }, + { + result: ExecutionResult + } + > +} -export type HookInputDecode = - & { result: ExecutionResult } - & InterfaceInput +export type HookDefDecode = { + input: + & { result: ExecutionResult } + & InterfaceInput +} -export type Hooks = { - encode: HookInputEncode - pack: HookInputPack - exchange: ExchangeInputHook - unpack: HookInputUnpack - decode: HookInputDecode +export type HookMap = { + encode: HookDefEncode + pack: HookDefPack + exchange: HookDefExchange + unpack: HookDefUnpack + decode: HookDefDecode } -export const anyware = Anyware.create({ +export const anyware = Anyware.create({ hookNamesOrderedBySequence, hooks: { - encode: ( - input, - ) => { - // console.log(`encode:1`) - let document: string | DocumentNode - switch (input.interface) { - case `raw`: { - document = input.document - break - } - case `typed`: { - // todo turn inputs into variables - document = SelectionSet.Print.rootTypeSelectionSet( - input.context, - getRootIndexOrThrow(input.context, input.rootTypeName), - input.selection, - ) - break - } - default: - throw casesExhausted(input) - } + encode: { + slots: { + body: (input) => { + return JSON.stringify({ + query: input.query, + variables: input.variables, + operationName: input.operationName, + }) + }, + }, + run: ({ input, slots }) => { + let document: string + let variables: StandardScalarVariables | undefined = undefined - // console.log(`encode:2`) - switch (input.transport) { - case `http`: { - return { - ...input, - transport: input.transport, - url: input.schema, - document, - variables: {}, - // operationName: '', + switch (input.interface) { + case `raw`: { + const documentPrinted = typeof input.document === `string` + ? input.document + : print(input.document) + document = documentPrinted + variables = input.variables + break } - } - case `memory`: { - return { - ...input, - transport: input.transport, - schema: input.schema, - document, - variables: {}, - // operationName: '', + case `typed`: { + // todo turn inputs into variables + variables = undefined + document = SelectionSet.Print.rootTypeSelectionSet( + input.context, + getRootIndexOrThrow(input.context, input.rootTypeName), + input.selection, + ) + break } + default: + throw casesExhausted(input) } - } - }, - pack: (input) => { - // console.log(`pack:1`) - const documentPrinted = typeof input.document === `string` - ? input.document - : print(input.document) - - switch (input.transport) { - case `http`: { - const body = { - query: documentPrinted, - variables: input.variables, - operationName: input.operationName, - } - const requestConfig: RequestInput = { - url: input.url, - method: `POST`, - headers: new Headers({ - 'accept': CONTENT_TYPE_GQL, - ...Object.fromEntries(new Headers(input.headers).entries()), - }), - body, + switch (input.transport) { + case `http`: { + return { + ...input, + url: input.schema, + body: slots.body({ + query: document, + variables, + operationName: `todo`, + }), + } } - - return { - ...input, - request: requestConfig, + case `memory`: { + return { + ...input, + schema: input.schema, + query: document, + variables, + // operationName: '', + } } } + }, + }, + pack: ({ input }) => { + switch (input.transport) { case `memory`: { + return input + } + case `http`: { + const headers = new Headers(input.headers) + headers.append(`accept`, CONTENT_TYPE_GQL) return { ...input, + request: { + url: input.url, + body: input.body, // JSON.stringify({ query, variables, operationName }), + method: `POST`, + headers, + }, } } default: throw casesExhausted(input) } }, - exchange: async (input) => { + exchange: async ({ input }) => { switch (input.transport) { case `http`: { const response = await fetch( new Request(input.request.url, { method: input.request.method, headers: input.request.headers, - body: JSON.stringify(input.request.body), + body: input.request.body, }), ) return { @@ -230,7 +248,7 @@ export const anyware = Anyware.create({ case `memory`: { const result = await execute({ schema: input.schema, - document: input.document, + document: input.query, variables: input.variables, operationName: input.operationName, }) @@ -243,7 +261,7 @@ export const anyware = Anyware.create({ throw casesExhausted(input) } }, - unpack: async (input) => { + unpack: async ({ input }) => { switch (input.transport) { case `http`: { const json = await input.response.json() as object @@ -263,7 +281,7 @@ export const anyware = Anyware.create({ throw casesExhausted(input) } }, - decode: (input) => { + decode: ({ input }) => { switch (input.interface) { // todo this depends on the return mode case `raw`: { diff --git a/src/layers/5_createExtension/createExtension.ts b/src/layers/5_createExtension/createExtension.ts index c1b809c0f..0c742381b 100644 --- a/src/layers/5_createExtension/createExtension.ts +++ b/src/layers/5_createExtension/createExtension.ts @@ -1,9 +1,3 @@ -import type { Anyware } from '../../lib/anyware/__.js' -import type { Core } from '../5_core/__.js' - -type Extension = { - name: string - anyware?: Anyware.Extension2 -} +import type { Extension } from '../5_client/client.js' export const createExtension = (input: Extension) => input diff --git a/src/layers/6_extensions/Upload/Upload.spec.ts b/src/layers/6_extensions/Upload/Upload.spec.ts new file mode 100644 index 000000000..ddc320f34 --- /dev/null +++ b/src/layers/6_extensions/Upload/Upload.spec.ts @@ -0,0 +1,51 @@ +import { processRequest } from 'graphql-upload-minimal' +import type { Server } from 'http' +import { createServer } from 'http' +import { afterAll, beforeAll, test } from 'vitest' +import { schema } from '../../../../tests/_/schemaUpload/schema.js' +import { Graffle } from '../../../entrypoints/alpha/main.js' +import type { StandardScalarVariables } from '../../../lib/graphql.js' +import { execute } from '../../0_functions/execute.js' +import { Upload } from './Upload.js' + +let server: Server + +beforeAll(() => { + // eslint-disable-next-line + server = createServer(async (request, response) => { + const body = await processRequest(request, response) + if (Array.isArray(body)) throw new Error(`Batch requests not supported.`) + const result = await execute({ + schema: schema, + document: body.query, + variables: body.variables as StandardScalarVariables, + operationName: body.operationName ?? undefined, + }) + response.write(JSON.stringify(result)) + response.statusCode = 200 + response.statusMessage = `OK` + }) + server.listen(3000) +}) + +afterAll(async () => { + await new Promise((resolve) => server.close(resolve)) +}) + +test(`upload`, async () => { + const graffle = Graffle.create({ + schema: new URL(`http://localhost:3000`), + }).use(Upload) + + const result = await graffle.raw({ + document: ` + mutation ($blob: Upload!) { + readTextFile(blob: $blob) + } + `, + variables: { + blob: new Blob([`Hello World`], { type: `text/plain` }) as any, // eslint-disable-line + }, + }) + console.log(result) +}) diff --git a/src/layers/6_extensions/Upload/Upload.ts b/src/layers/6_extensions/Upload/Upload.ts new file mode 100644 index 000000000..4474ddb5a --- /dev/null +++ b/src/layers/6_extensions/Upload/Upload.ts @@ -0,0 +1,63 @@ +import type { StandardScalarVariables } from '../../../lib/graphql.js' +import type { ExecutionInput } from '../../../lib/graphqlHTTP.js' +import { createExtension } from '../../5_createExtension/createExtension.js' +import extractFiles from './extractFiles.js' + +/** + * @see https://github.com/jaydenseric/graphql-multipart-request-spec + */ +export const Upload = createExtension({ + name: `Upload`, + anyware: async ({ encode }) => { + // todo encode is async, fixme. + return await encode({ + using: { + body: (input) => { + if (!(input.variables && isUsingUploadScalar(input.variables))) return + + // TODO we can probably get file upload working for in-memory schemas too :) + if (encode.input.transport !== `http`) throw new Error(`Must use http transport for uploads.`) + + return createUploadBody({ + query: input.query, + variables: input.variables, + }) + }, + }, + }) + }, +}) + +const createUploadBody = (input: ExecutionInput): FormData => { + const { clone, files } = extractFiles( + { query: input.query, variables: input.variables }, + (value: unknown) => value instanceof Blob, + ``, + ) + const operationJSON = JSON.stringify(clone) + console.log(clone, files) + + if (files.size === 0) throw new Error(`Not an upload request.`) + + const form = new FormData() + + form.append(`operations`, operationJSON) + + const map: Record = {} + let i = 0 + for (const paths of files.values()) { + map[++i] = paths + } + form.append(`map`, JSON.stringify(map)) + + i = 0 + for (const file of files.keys()) { + form.append(`${++i}`, file) + } + + return form +} + +const isUsingUploadScalar = (variables: StandardScalarVariables) => { + return true // todo +} diff --git a/src/layers/6_extensions/Upload/extractFiles.ts b/src/layers/6_extensions/Upload/extractFiles.ts new file mode 100644 index 000000000..655501317 --- /dev/null +++ b/src/layers/6_extensions/Upload/extractFiles.ts @@ -0,0 +1,198 @@ +/* eslint-disable */ + +import isPlainObject from 'is-plain-obj' + +/** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */ + +/** + * Recursively extracts files and their {@link ObjectPath object paths} within a + * value, replacing them with `null` in a deep clone without mutating the + * original value. + * [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/Filelist) + * instances are treated as + * [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) instance + * arrays. + * @template Extractable Extractable file type. + * @param {unknown} value Value to extract files from. Typically an object tree. + * @param {(value: unknown) => value is Extractable} isExtractable Matches + * extractable files. Typically {@linkcode isExtractableFile}. + * @param {ObjectPath} [path] Prefix for object paths for extracted files. + * Defaults to `""`. + * @returns {Extraction} Extraction result. + * @example + * Extracting files from an object. + * + * For the following: + * + * ```js + * import extractFiles from "extract-files/extractFiles.mjs"; + * import isExtractableFile from "extract-files/isExtractableFile.mjs"; + * + * const file1 = new File(["1"], "1.txt", { type: "text/plain" }); + * const file2 = new File(["2"], "2.txt", { type: "text/plain" }); + * const value = { + * a: file1, + * b: [file1, file2], + * }; + * + * const { clone, files } = extractFiles(value, isExtractableFile, "prefix"); + * ``` + * + * `value` remains the same. + * + * `clone` is: + * + * ```json + * { + * "a": null, + * "b": [null, null] + * } + * ``` + * + * `files` is a + * [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + * instance containing: + * + * | Key | Value | + * | :------ | :--------------------------- | + * | `file1` | `["prefix.a", "prefix.b.0"]` | + * | `file2` | `["prefix.b.1"]` | + */ +export default function extractFiles(value: any, isExtractable: any, path = ``) { + if (!arguments.length) throw new TypeError(`Argument 1 \`value\` is required.`) + + if (typeof isExtractable !== `function`) { + throw new TypeError(`Argument 2 \`isExtractable\` must be a function.`) + } + + if (typeof path !== `string`) { + throw new TypeError(`Argument 3 \`path\` must be a string.`) + } + + /** + * Deeply clonable value. + * @typedef {Array | FileList | { + * [key: PropertyKey]: unknown + * }} Cloneable + */ + + /** + * Clone of a {@link Cloneable deeply cloneable value}. + * @typedef {Exclude} Clone + */ + + /** + * Map of values recursed within the input value and their clones, for reusing + * clones of values that are referenced multiple times within the input value. + * @type {Map} + */ + const clones = new Map() + + /** + * Extracted files and their object paths within the input value. + * @type {Extraction["files"]} + */ + const files = new Map() + + /** + * Recursively clones the value, extracting files. + * @param {unknown} value Value to extract files from. + * @param {ObjectPath} path Prefix for object paths for extracted files. + * @param {Set} recursed Recursed values for avoiding infinite + * recursion of circular references within the input value. + * @returns {unknown} Clone of the value with files replaced with `null`. + */ + function recurse(value: any, path: any, recursed: any) { + if (isExtractable(value)) { + const filePaths = files.get(value) + + filePaths ? filePaths.push(path) : files.set(value, [path]) + + return null + } + + const valueIsList = Array.isArray(value) + || (typeof FileList !== `undefined` && value instanceof FileList) + const valueIsPlainObject = isPlainObject(value) + + if (valueIsList || valueIsPlainObject) { + let clone = clones.get(value) + + const uncloned = !clone + + if (uncloned) { + clone = valueIsList + ? [] + // Replicate if the plain object is an `Object` instance. + : value instanceof /** @type {any} */ (Object) + ? {} + : Object.create(null) + + clones.set(value, /** @type {Clone} */ (clone)) + } + + if (!recursed.has(value)) { + const pathPrefix = path ? `${path}.` : `` + const recursedDeeper = new Set(recursed).add(value) + + if (valueIsList) { + let index = 0 + + for (const item of value) { + const itemClone = recurse( + item, + pathPrefix + index++, + recursedDeeper, + ) + + if (uncloned) /** @type {Array} */ (clone).push(itemClone) + } + } else { + for (const key in value) { + const propertyClone = recurse( + value[key], + pathPrefix + key, + recursedDeeper, + ) + + if (uncloned) { + /** @type {{ [key: PropertyKey]: unknown }} */ + clone[key] = propertyClone + } + } + } + } + + return clone + } + + return value + } + + return { + clone: recurse(value, path, new Set()), + files, + } +} + +/** + * An extraction result. + * @template [Extractable=unknown] Extractable file type. + * @typedef {object} Extraction + * @prop {unknown} clone Clone of the original value with extracted files + * recursively replaced with `null`. + * @prop {Map>} files Extracted files and their + * object paths within the original value. + */ + +/** + * String notation for the path to a node in an object tree. + * @typedef {string} ObjectPath + * @see [`object-path` on npm](https://npm.im/object-path). + * @example + * An object path for object property `a`, array index `0`, object property `b`: + * + * ``` + * a.0.b + * ``` + */ diff --git a/src/layers/6_extensions/upload.ts b/src/layers/6_extensions/upload.ts deleted file mode 100644 index 82b432e97..000000000 --- a/src/layers/6_extensions/upload.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { StandardScalarVariables } from '../../lib/graphql.js' -import type { ExecutionInput } from '../../lib/graphqlHTTP.js' -import { createExtension } from '../5_createExtension/createExtension.js' - -export const Upload = createExtension({ - name: `Upload`, - anyware: async ({ pack }) => { - if (!isUsingUploadScalar(pack.input.variables)) return pack() - - const { exchange } = pack() - - // TODO we can probably get file upload working for in-memory schemas too :) - if (exchange.input.transport !== `http`) throw new Error(`Must use http transport for uploads.`) - - const headers = new Headers(exchange.input.request.headers) - headers.append(`content-type`, `multipart/form-data`) - - return exchange({ - ...exchange.input, - request: { - ...exchange.input.request, - body: createUploadBody({ - query: exchange.input.request.body.query, - variables: exchange.input.request.body.variables, - }), - headers, - }, - }) - }, -}) - -/** @see https://github.com/lynxtaa/awesome-graphql-client/blob/a3ecec2fdd3b72b005268a24b2364dd647dc6efd/packages/awesome-graphql-client/src/AwesomeGraphQLClient.ts#L79 */ -const createUploadBody = (input: ExecutionInput): ExecutionInput => { - return `todo` -} - -const isUsingUploadScalar = (variables: StandardScalarVariables) => { - return `todo` -} diff --git a/src/lib/anyware/main.ts b/src/lib/anyware/main.ts index c221d8bc5..7b7e6e8b1 100644 --- a/src/lib/anyware/main.ts +++ b/src/lib/anyware/main.ts @@ -29,7 +29,7 @@ export type Extension2< type ExtensionHooks< $HookSequence extends HookSequence, - $HookMap extends Record<$HookSequence[number], object> = Record<$HookSequence[number], object>, + $HookMap extends Record<$HookSequence[number], HookDef> = Record<$HookSequence[number], HookDef>, $Result = unknown, $Options extends ExtensionOptions = ExtensionOptions, > = { @@ -37,7 +37,7 @@ type ExtensionHooks< } type CoreInitialInput<$Core extends Core> = - $Core[PrivateTypesSymbol]['hookMap'][$Core[PrivateTypesSymbol]['hookSequence'][0]] + $Core[PrivateTypesSymbol]['hookMap'][$Core[PrivateTypesSymbol]['hookSequence'][0]]['input'] const PrivateTypesSymbol = Symbol(`private`) @@ -51,21 +51,22 @@ export type SomeHookEnvelope = { [name: string]: SomeHook } -export type SomeHook any = (input: any) => any> = fn & { +export type SomeHook any = (input: { input: any }) => any> = fn & { [hookSymbol]: HookSymbol // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. // E.g. adding `| unknown` would destroy the knowledge of hook envelope case // todo this is not strictly true, it could also be the final result - // TODO how do I make this input type object without breaking the final types in e.g. client.extend.test - // Ask Pierre - // (input: object): SomeHookEnvelope - input: Parameters[0] + input: Parameters[0]['input'] } export type HookMap<$HookSequence extends HookSequence> = Record< $HookSequence[number], - any /* object <- type error but more accurate */ + HookDef > +export type HookDef = { + input: any /* object <- type error but more accurate */ + slots?: any /* object <- type error but more accurate */ +} type Hook< $HookSequence extends HookSequence, @@ -74,21 +75,29 @@ type Hook< $Name extends $HookSequence[number] = $HookSequence[number], $Options extends ExtensionOptions = ExtensionOptions, > = - & (<$$Input extends $HookMap[$Name]>( - input?: $$Input, + & (<$$Input extends $HookMap[$Name]['input']>( + input?: { + input?: $$Input + } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), ) => HookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>) & { [hookSymbol]: HookSymbol - input: $HookMap[$Name] + input: $HookMap[$Name]['input'] } +type SlotInputify<$Slots extends Record any>> = { + [K in keyof $Slots]: SlotInput<$Slots[K]> +} + +type SlotInput any> = (...args: Parameters) => ReturnType | undefined + type HookReturn< $HookSequence extends HookSequence, $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, $Result = unknown, $Name extends $HookSequence[number] = $HookSequence[number], $Options extends ExtensionOptions = ExtensionOptions, -> = +> = Promise< | ($Options['retrying'] extends true ? Error : never) | (IsLastValue<$Name, $HookSequence> extends true ? $Result : { [$NameNext in FindValueAfter<$Name, $HookSequence>]: Hook< @@ -98,6 +107,7 @@ type HookReturn< $NameNext > }) +> export type Core< $HookSequence extends HookSequence = HookSequence, @@ -111,11 +121,44 @@ export type Core< } hookNamesOrderedBySequence: $HookSequence hooks: { - [$HookName in $HookSequence[number]]: ( - input: $HookMap[$HookName], - ) => MaybePromise< - IsLastValue<$HookName, $HookSequence> extends true ? $Result : $HookMap[FindValueAfter<$HookName, $HookSequence>] - > + [$HookName in $HookSequence[number]]: { + slots: $HookMap[$HookName]['slots'] + run: (input: { + input: $HookMap[$HookName]['input'] + slots: $HookMap[$HookName]['slots'] + }) => MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>] + > + } + } +} + +export type CoreInput< + $HookSequence extends HookSequence = HookSequence, + $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, + $Result = unknown, +> = { + hookNamesOrderedBySequence: $HookSequence + hooks: { + [$HookName in $HookSequence[number]]: keyof $HookMap[$HookName]['slots'] extends never ? (input: { + input: $HookMap[$HookName]['input'] + slots: $HookMap[$HookName]['slots'] + } + ) => MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] + > + : { + slots: $HookMap[$HookName]['slots'] + run: (input: { + input: $HookMap[$HookName]['input'] + slots: $HookMap[$HookName]['slots'] + }) => MaybePromise< + IsLastValue<$HookName, $HookSequence> extends true ? $Result + : $HookMap[FindValueAfter<$HookName, $HookSequence>]['input'] + > + } } } @@ -179,7 +222,7 @@ const createPassthrough = (hookName: string) => async (hookEnvelope: SomeHookEnv if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) } - return await hook(hook.input) + return await hook({ input: hook.input }) } type Config = Required @@ -214,11 +257,18 @@ export const create = < $HookMap extends HookMap<$HookSequence> = HookMap<$HookSequence>, $Result = unknown, >( - coreInput: Omit, PrivateTypesSymbol>, + coreInput: CoreInput<$HookSequence, $HookMap, $Result>, ): Builder> => { type $Core = Core<$HookSequence, $HookMap, $Result> - const core = coreInput as any as $Core + const core = { + ...coreInput, + hooks: Object.fromEntries( + Object.entries(coreInput.hooks).map(([k, v]) => { + return [k, typeof v === `function` ? { slots: {}, run: v } : v] + }), + ), + } as any as $Core const builder: Builder<$Core> = { core, @@ -256,7 +306,7 @@ const toInternalExtension = (core: Core, config: Config, extension: ExtensionInp const retrying = typeof extension === `function` ? false : extension.retrying const applyBody = async (input: object) => { try { - const result = await extensionRun(input) + const result = await extensionRun({ input }) body.resolve(result) } catch (error) { body.reject(error) diff --git a/tests/_/schemaUpload/schema.ts b/tests/_/schemaUpload/schema.ts new file mode 100644 index 000000000..a764932c9 --- /dev/null +++ b/tests/_/schemaUpload/schema.ts @@ -0,0 +1,36 @@ +import SchemaBuilder from '@pothos/core' + +const builder = new SchemaBuilder<{ + Scalars: { Upload: { Input: Blob; Output: never } } +}>({}) + +builder.scalarType(`Upload`, { + serialize: () => { + throw new Error(`Uploads can only be used as input types`) + }, +}) + +builder.queryType({ + fields: t => ({ + greetings: t.string({ resolve: () => `Hello World` }), + }), +}) + +builder.mutationType({ + fields: t => ({ + readTextFile: t.string({ + args: { + blob: t.arg({ + type: `Upload`, + required: true, + }), + }, + resolve: async (_, { blob }) => { + const textContent = await blob.text() + return textContent + }, + }), + }), +}) + +export const schema = builder.toSchema({}) diff --git a/tests/_/text.txt b/tests/_/text.txt new file mode 100644 index 000000000..5adcd3d86 --- /dev/null +++ b/tests/_/text.txt @@ -0,0 +1 @@ +This is a text file. From b05c0e7f9356696fab8b17a2317192f934fdd424 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 11:15:34 -0400 Subject: [PATCH 03/11] fix existing tests --- src/layers/6_extensions/Upload/Upload.ts | 16 ++- src/lib/anyware/__.test-d.ts | 124 +++++++++++++++-------- src/lib/anyware/main.test.ts | 60 +++++------ src/lib/anyware/main.ts | 2 +- src/lib/anyware/runHook.ts | 22 ++-- src/lib/anyware/specHelpers.ts | 22 ++-- src/lib/prelude.ts | 2 +- 7 files changed, 156 insertions(+), 92 deletions(-) diff --git a/src/layers/6_extensions/Upload/Upload.ts b/src/layers/6_extensions/Upload/Upload.ts index 4474ddb5a..3dd980e1e 100644 --- a/src/layers/6_extensions/Upload/Upload.ts +++ b/src/layers/6_extensions/Upload/Upload.ts @@ -9,8 +9,7 @@ import extractFiles from './extractFiles.js' export const Upload = createExtension({ name: `Upload`, anyware: async ({ encode }) => { - // todo encode is async, fixme. - return await encode({ + const { pack } = await encode({ using: { body: (input) => { if (!(input.variables && isUsingUploadScalar(input.variables))) return @@ -25,6 +24,19 @@ export const Upload = createExtension({ }, }, }) + + // const { exchange } = await pack() + // if (exchange.input.transport !== `http`) return exchange() + + // return await exchange({ + // input: { + // ...exchange.input, + // request: { + // ...exchange.input.request, + // headers: {}, + // }, + // }, + // }) }, }) diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts index 6d05d19ac..6278aeecb 100644 --- a/src/lib/anyware/__.test-d.ts +++ b/src/lib/anyware/__.test-d.ts @@ -1,56 +1,100 @@ /* eslint-disable */ -import { run } from 'node:test' -import { expectTypeOf, test } from 'vitest' +import { describe, expectTypeOf, test } from 'vitest' import { Result } from '../../../tests/_/schema/generated/SchemaRuntime.js' import { ContextualError } from '../errors/ContextualError.js' import { MaybePromise } from '../prelude.js' import { Anyware } from './__.js' -import { ResultEnvelop, SomeHook } from './main.js' +import { SomeHook } from './main.js' type InputA = { valueA: string } type InputB = { valueB: string } type Result = { return: string } -const create = Anyware.create<['a', 'b'], { a: InputA; b: InputB }, Result> - -test('create', () => { - expectTypeOf(create).toMatchTypeOf< - (input: { - hookNamesOrderedBySequence: ['a', 'b'] - hooks: { - a: (input: InputA) => InputB - b: (input: InputB) => Result - } - }) => any - >() +const create = Anyware.create<['a', 'b'], { a: { input: InputA }; b: { input: InputB } }, Result> + +describe('without slots', () => { + test('create', () => { + expectTypeOf(create).toMatchTypeOf< + (input: { + hookNamesOrderedBySequence: ['a', 'b'] + hooks: { + a: (input: { input: InputA }) => InputB + b: (input: { input: InputB }) => Result + } + }) => any + >() + }) + + test('run', () => { + type run = ReturnType['run'] + + expectTypeOf().toMatchTypeOf< + (input: { + initialInput: InputA + options?: Anyware.Options + retryingExtension?: (input: { + a: SomeHook< + (input?: { input?: InputA }) => MaybePromise< + Error | { + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + } + > + > + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + }) => Promise + extensions: ((input: { + a: SomeHook< + (input?: { input?: InputA }) => MaybePromise<{ + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + }> + > + b: SomeHook<(input?: { input?: InputB }) => MaybePromise> + }) => Promise)[] + }) => Promise + >() + }) }) -test('run', () => { - type run = ReturnType['run'] - - expectTypeOf().toMatchTypeOf< - (input: { - initialInput: InputA - options?: Anyware.Options - retryingExtension?: (input: { - a: SomeHook< - (input?: InputA) => MaybePromise< - Error | { - b: SomeHook<(input?: InputB) => MaybePromise> +describe('withSlots', () => { + const create = Anyware.create<['a'], { a: { input: InputA; slots: { x: (x: boolean) => number } } }, Result> + + test('create', () => { + expectTypeOf(create).toMatchTypeOf< + (input: { + hookNamesOrderedBySequence: ['a'] + hooks: { + a: { + run: (input: { input: InputA }) => Result + slots: { + x: (x: boolean) => number } + } + } + }) => any + >() + }) + + test('run', () => { + type run = ReturnType['run'] + + expectTypeOf().toMatchTypeOf< + (input: { + initialInput: InputA + options?: Anyware.Options + retryingExtension?: (input: { + a: SomeHook< + (input?: { input?: InputA; using: { x?: (x: boolean) => number | undefined } }) => MaybePromise< + Error | Result + > + > + }) => Promise + extensions: ((input: { + a: SomeHook< + (input?: { input?: InputA }) => MaybePromise > - > - b: SomeHook<(input?: InputB) => MaybePromise> - }) => Promise - extensions: ((input: { - a: SomeHook< - (input?: InputA) => MaybePromise<{ - b: SomeHook<(input?: InputB) => MaybePromise> - }> - > - b: SomeHook<(input?: InputB) => MaybePromise> - }) => Promise)[] - }) => Promise - >() + }) => Promise)[] + }) => Promise + >() + }) }) diff --git a/src/lib/anyware/main.test.ts b/src/lib/anyware/main.test.ts index 8be1e4642..35c3cfcf2 100644 --- a/src/lib/anyware/main.test.ts +++ b/src/lib/anyware/main.test.ts @@ -18,12 +18,12 @@ describe(`one extension`, () => { expect( await run(async ({ a }) => { const { b } = await a(a.input) - await b(b.input) + await b({ input: b.input }) return 0 }), ).toEqual(0) - expect(core.hooks.a).toHaveBeenCalled() - expect(core.hooks.b).toHaveBeenCalled() + expect(core.hooks.a.run).toHaveBeenCalled() + expect(core.hooks.b.run).toHaveBeenCalled() }) test('can call hook with no input, making the original input be used', () => { expect( @@ -43,8 +43,8 @@ describe(`one extension`, () => { return a.input }), ).toEqual({ value: `initial` }) - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`at start, return own result`, async () => { expect( @@ -53,38 +53,38 @@ describe(`one extension`, () => { return 0 }), ).toEqual(0) - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`after first hook, return own result`, async () => { expect( await run(async ({ a }) => { - const { b } = await a(a.input) + const { b } = await a({ input: a.input }) return b.input.value + `+x` }), ).toEqual(`initial+a+x`) - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) }) describe(`can partially apply`, () => { test(`only first hook`, async () => { expect( await run(async ({ a }) => { - return await a({ value: a.input.value + `+ext` }) + return await a({ input: { value: a.input.value + `+ext` } }) }), ).toEqual({ value: `initial+ext+a+b` }) }) test(`only second hook`, async () => { expect( await run(async ({ b }) => { - return await b({ value: b.input.value + `+ext` }) + return await b({ input: { value: b.input.value + `+ext` } }) }), ).toEqual({ value: `initial+a+ext+b` }) }) test(`only second hook + end`, async () => { expect( await run(async ({ b }) => { - const result = await b({ value: b.input.value + `+ext` }) + const result = await b({ input: { value: b.input.value + `+ext` } }) return result.value + `+end` }), ).toEqual(`initial+a+ext+b+end`) @@ -99,35 +99,35 @@ describe(`two extensions`, () => { const ex2 = vi.fn().mockImplementation(() => 2) expect(await run(ex1, ex2)).toEqual(1) expect(ex2).not.toHaveBeenCalled() - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`each can adjust first hook then passthrough`, async () => { - const ex1 = ({ a }: any) => a({ value: a.input.value + `+ex1` }) - const ex2 = ({ a }: any) => a({ value: a.input.value + `+ex2` }) + const ex1 = ({ a }: any) => a({ input: { value: a.input.value + `+ex1` } }) + const ex2 = ({ a }: any) => a({ input: { value: a.input.value + `+ex2` } }) expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+ex2+a+b` }) }) test(`each can adjust each hook`, async () => { const ex1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) - return await b({ value: b.input.value + `+ex1` }) + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + return await b({ input: { value: b.input.value + `+ex1` } }) } const ex2 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex2` }) - return await b({ value: b.input.value + `+ex2` }) + const { b } = await a({ input: { value: a.input.value + `+ex2` } }) + return await b({ input: { value: b.input.value + `+ex2` } }) } expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+ex2+a+ex1+ex2+b` }) }) test(`second can skip hook a`, async () => { const ex1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) - return await b({ value: b.input.value + `+ex1` }) + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) + return await b({ input: { value: b.input.value + `+ex1` } }) } const ex2 = async ({ b }: any) => { - return await b({ value: b.input.value + `+ex2` }) + return await b({ input: { value: b.input.value + `+ex2` } }) } expect(await run(ex1, ex2)).toEqual({ value: `initial+ex1+a+ex1+ex2+b` }) }) @@ -142,13 +142,13 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual(2) expect(ex1AfterA).toBe(false) - expect(core.hooks.a).not.toHaveBeenCalled() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).not.toHaveBeenCalled() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) test(`second can short-circuit after hook a`, async () => { let ex1AfterB = false const ex1 = async ({ a }: any) => { - const { b } = await a({ value: a.input.value + `+ex1` }) + const { b } = await a({ input: { value: a.input.value + `+ex1` } }) await b({ value: b.input.value + `+ex1` }) ex1AfterB = true } @@ -158,8 +158,8 @@ describe(`two extensions`, () => { } expect(await run(ex1, ex2)).toEqual(2) expect(ex1AfterB).toBe(false) - expect(core.hooks.a).toHaveBeenCalledOnce() - expect(core.hooks.b).not.toHaveBeenCalled() + expect(core.hooks.a.run).toHaveBeenCalledOnce() + expect(core.hooks.b.run).not.toHaveBeenCalled() }) }) @@ -206,7 +206,7 @@ describe(`errors`, () => { }) test(`if implementation fails, without extensions, result is the error`, async () => { - core.hooks.a.mockReset().mockRejectedValueOnce(oops) + core.hooks.a.run.mockReset().mockRejectedValueOnce(oops) const result = await run() as ContextualError expect({ result, @@ -246,7 +246,7 @@ describe(`errors`, () => { describe('retrying extension', () => { test('if hook fails, extension can retry, then short-circuit', async () => { - core.hooks.a.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) + core.hooks.a.run.mockReset().mockRejectedValueOnce(oops).mockResolvedValueOnce(1) const result = await run(createRetryingExtension(async function foo({ a }) { const result1 = await a() expect(result1).toEqual(oops) diff --git a/src/lib/anyware/main.ts b/src/lib/anyware/main.ts index 7b7e6e8b1..ee1f909cb 100644 --- a/src/lib/anyware/main.ts +++ b/src/lib/anyware/main.ts @@ -306,7 +306,7 @@ const toInternalExtension = (core: Core, config: Config, extension: ExtensionInp const retrying = typeof extension === `function` ? false : extension.retrying const applyBody = async (input: object) => { try { - const result = await extensionRun({ input }) + const result = await extensionRun(input) body.resolve(result) } catch (error) { body.reject(error) diff --git a/src/lib/anyware/runHook.ts b/src/lib/anyware/runHook.ts index a28048f4b..b9213ab63 100644 --- a/src/lib/anyware/runHook.ts +++ b/src/lib/anyware/runHook.ts @@ -1,6 +1,6 @@ import { Errors } from '../errors/__.js' -import type { Deferred } from '../prelude.js' -import { casesExhausted, createDeferred, debug, debugSub, errorFromMaybeError } from '../prelude.js' +import type { Deferred, SomeFunction } from '../prelude.js' +import { casesExhausted, createDeferred, debugSub, errorFromMaybeError } from '../prelude.js' import type { Core, Extension, ResultEnvelop, SomeHookEnvelope } from './main.js' type HookDoneResolver = (input: HookResult) => void @@ -88,9 +88,9 @@ export const runHook = async ( debugExtension(`start`) let hookFailed = false const hook = createHook(originalInput, (extensionInput) => { - debugExtension(`extension calls this hook`) + debugExtension(`extension calls this hook`, extensionInput) - const inputResolved = extensionInput ?? originalInput + const inputResolved = extensionInput?.input ?? originalInput // [1] // Never resolve this hook call, the extension is in an invalid state and should not continue executing. @@ -140,7 +140,8 @@ export const runHook = async ( const envelop_ = envelope as SomeHookEnvelope // todo ... better way? const hook = envelop_[name] if (!hook) throw new Error(`Hook not found in envelope: ${name}`) - const result = await hook(extensionInput ?? originalInput) as Promise< + // todo use inputResolved ? + const result = await hook({ ...extensionInput, input: extensionInput?.input ?? originalInput }) as Promise< SomeHookEnvelope | Error | ResultEnvelop > return result @@ -201,7 +202,7 @@ export const runHook = async ( return } case `extensionReturned`: { - debug(`${name}: ${extension.name}: extension returned`) + debugExtension(`extension returned`) if (result === envelope) { void runHook({ core, @@ -218,7 +219,8 @@ export const runHook = async ( return } case `extensionThrew`: { - debug(`${name}: ${extension.name}: extension threw`) + debugExtension(`extension threw`) + console.error(result) done({ type: `error`, hookName: name, @@ -229,7 +231,7 @@ export const runHook = async ( return } case `hookInvokedButThrew`: - debug(`${name}: ${extension.name}: hook error`) + debugExtension(`hook error`) // todo rename source to "hook" done({ type: `error`, hookName: name, source: `implementation`, error: errorFromMaybeError(result) }) return @@ -246,7 +248,7 @@ export const runHook = async ( let result try { - result = await implementation(originalInput as any) + result = await implementation.run(originalInput as any) } catch (error) { debugHook(`implementation error`) const lastExtension = nextExtensionsStack[nextExtensionsStack.length - 1] @@ -266,7 +268,7 @@ export const runHook = async ( } } -const createHook = <$X, $F extends (input?: object) => any>( +const createHook = <$X, $F extends (input?: { input?: object; slots?: Record }) => any>( originalInput: $X, fn: $F, ): $F & { input: $X } => { diff --git a/src/lib/anyware/specHelpers.ts b/src/lib/anyware/specHelpers.ts index 5568f30d9..77c3b1249 100644 --- a/src/lib/anyware/specHelpers.ts +++ b/src/lib/anyware/specHelpers.ts @@ -10,18 +10,24 @@ export const initialInput: Input = { value: `initial` } type $Core = ReturnType & { hooks: { - a: Mock - b: Mock + a: { run: Mock; slots: {} } // eslint-disable-line + b: { run: Mock; slots: {} } // eslint-disable-line } } export const createAnyware = () => { - const a = vi.fn().mockImplementation((input: Input) => { - return { value: input.value + `+a` } - }) - const b = vi.fn().mockImplementation((input: Input) => { - return { value: input.value + `+b` } - }) + const a = { + slots: {}, + run: vi.fn().mockImplementation((input: Input) => { + return { value: input.value + `+a` } + }), + } + const b = { + slots: {}, + run: vi.fn().mockImplementation((input: Input) => { + return { value: input.value + `+b` } + }), + } return Anyware.create<['a', 'b'], Anyware.HookMap<['a', 'b']>, Input>({ hookNamesOrderedBySequence: [`a`, `b`], diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index 109a9784e..8bb0dbbbe 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -241,7 +241,7 @@ export const capitalizeFirstLetter = (string: string) => string.charAt(0).toUppe export type SomeAsyncFunction = (...args: unknown[]) => Promise -export type SomeMaybeAsyncFunction = (...args: unknown[]) => MaybePromise +export type SomeFunction = (...args: unknown[]) => MaybePromise export type Deferred = { promise: Promise From b868b53ecda9942d53eff93164c931cba23ad07b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 12:22:18 -0400 Subject: [PATCH 04/11] basic slot implementation --- src/layers/5_client/client.extend.test.ts | 4 +-- src/layers/5_client/client.ts | 5 ++- src/lib/anyware/main.test.ts | 19 ++++++++++ src/lib/anyware/main.ts | 5 ++- src/lib/anyware/runHook.ts | 21 +++++++++--- src/lib/anyware/runPipeline.ts | 1 + src/lib/anyware/specHelpers.ts | 42 +++++++++++++++++------ 7 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/layers/5_client/client.extend.test.ts b/src/layers/5_client/client.extend.test.ts index 77cb3aba5..9485cb37a 100644 --- a/src/layers/5_client/client.extend.test.ts +++ b/src/layers/5_client/client.extend.test.ts @@ -22,7 +22,7 @@ describe(`entrypoint request`, () => { // todo these input type tests should be moved down to Anyware // expectTypeOf(exchange).toEqualTypeOf() // expect(exchange.input).toEqual({ url: 'https://foo', document: `query { id \n }` }) - return await pack({ ...pack.input, headers }) + return await pack({ input: { ...pack.input, headers } }) }) expect(await client2.query.id()).toEqual(db.id) }) @@ -31,7 +31,7 @@ describe(`entrypoint request`, () => { return createResponse({ data: { id: db.id } }) }) const client2 = client.use(async ({ pack }) => { - const { exchange } = await pack({ ...pack.input, headers }) + const { exchange } = await pack({ input: { ...pack.input, headers } }) return await exchange(exchange.input) }) expect(await client2.query.id()).toEqual(db.id) diff --git a/src/layers/5_client/client.ts b/src/layers/5_client/client.ts index 64c916258..ac2345e18 100644 --- a/src/layers/5_client/client.ts +++ b/src/layers/5_client/client.ts @@ -303,7 +303,10 @@ export const createInternal = ( const contextWithReturnModeSet = updateContextConfig(context, { returnMode: `graphqlSuccess` }) return await runRaw(contextWithReturnModeSet, rawInput) }, - use: (extension: Extension) => { + use: (extensionOrAnyware: Extension | Anyware.Extension2) => { + const extension = typeof extensionOrAnyware === `function` + ? { anyware: extensionOrAnyware, name: extensionOrAnyware.name } + : extensionOrAnyware // todo test that adding extensions returns a copy of client return createInternal(input, { extensions: [...state.extensions, extension] }) }, diff --git a/src/lib/anyware/main.test.ts b/src/lib/anyware/main.test.ts index 35c3cfcf2..131c2bfac 100644 --- a/src/lib/anyware/main.test.ts +++ b/src/lib/anyware/main.test.ts @@ -22,6 +22,7 @@ describe(`one extension`, () => { return 0 }), ).toEqual(0) + expect(core.hooks.a.run.mock.calls[0]).toMatchObject([{ input: { value: `initial` } }]) expect(core.hooks.a.run).toHaveBeenCalled() expect(core.hooks.b.run).toHaveBeenCalled() }) @@ -308,3 +309,21 @@ describe('retrying extension', () => { }) }) }) + +describe('slots', () => { + test('have defaults that are called by default', async () => { + await run() + expect(core.hooks.a.slots.append.mock.calls[0]).toMatchObject(['a']) + expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) + }) + test('extension can provide own function to slot on just one of a set of hooks', async () => { + const result = await run(async ({ a }) => { + return a({ slots: { append: () => 'x' } }) + }) + expect(core.hooks.a.slots.append).not.toBeCalled() + expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) + expect(result).toEqual({ value: 'initial+x+b' }) + }) + // todo hook with two slots + // todo two extensions, each using the slot (later one should win) +}) diff --git a/src/lib/anyware/main.ts b/src/lib/anyware/main.ts index ee1f909cb..689ef4eb9 100644 --- a/src/lib/anyware/main.ts +++ b/src/lib/anyware/main.ts @@ -222,7 +222,7 @@ const createPassthrough = (hookName: string) => async (hookEnvelope: SomeHookEnv if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) } - return await hook({ input: hook.input }) + return await hook({ input: hook.input, slots: hook.slots }) } type Config = Required @@ -272,8 +272,7 @@ export const create = < const builder: Builder<$Core> = { core, - run: async (input) => { - const { initialInput, extensions, options, retryingExtension } = input + run: async ({ initialInput, extensions, options, retryingExtension }) => { const extensions_ = retryingExtension ? [...extensions, createRetryingExtension(retryingExtension)] : extensions const initialHookStackAndErrors = extensions_.map(extension => toInternalExtension(core, resolveOptions(options), extension) diff --git a/src/lib/anyware/runHook.ts b/src/lib/anyware/runHook.ts index b9213ab63..22b8bfea0 100644 --- a/src/lib/anyware/runHook.ts +++ b/src/lib/anyware/runHook.ts @@ -29,11 +29,14 @@ export type HookResultErrorImplementation = { error: Error } +type Slots = Record + type Input = { core: Core name: string done: HookDoneResolver originalInput: unknown + customSlots: Slots /** * The extensions that are at this hook awaiting. */ @@ -55,7 +58,7 @@ const createExecutableChunk = <$Extension extends Extension>(extension: $Extensi }) export const runHook = async ( - { core, name, done, originalInput, extensionsStack, nextExtensionsStack, asyncErrorDeferred }: Input, + { core, name, done, originalInput, extensionsStack, nextExtensionsStack, asyncErrorDeferred, customSlots }: Input, ) => { const debugHook = debugSub(`hook ${name}:`) @@ -91,6 +94,10 @@ export const runHook = async ( debugExtension(`extension calls this hook`, extensionInput) const inputResolved = extensionInput?.input ?? originalInput + const customSlotsResolved = { + ...customSlots, + ...extensionInput?.slots, + } // [1] // Never resolve this hook call, the extension is in an invalid state and should not continue executing. @@ -135,6 +142,7 @@ export const runHook = async ( asyncErrorDeferred, extensionsStack: [extensionRetry], nextExtensionsStack, + customSlots: customSlotsResolved, }) return extensionRetry.currentChunk.promise.then(async (envelope) => { const envelop_ = envelope as SomeHookEnvelope // todo ... better way? @@ -159,6 +167,7 @@ export const runHook = async ( originalInput: inputResolved, extensionsStack: extensionsStackRest, nextExtensionsStack: nextNextHookStack, + customSlots: customSlotsResolved, }) return extensionWithNextChunk.currentChunk.promise.then(_ => { @@ -212,6 +221,7 @@ export const runHook = async ( asyncErrorDeferred, extensionsStack: extensionsStackRest, nextExtensionsStack, + customSlots, }) } else { done({ type: `shortCircuited`, result }) @@ -220,7 +230,6 @@ export const runHook = async ( } case `extensionThrew`: { debugExtension(`extension threw`) - console.error(result) done({ type: `error`, hookName: name, @@ -248,7 +257,11 @@ export const runHook = async ( let result try { - result = await implementation.run(originalInput as any) + const slotsResolved = { + ...implementation.slots, + ...customSlots, + } + result = await implementation.run({ input: originalInput as any, slots: slotsResolved }) } catch (error) { debugHook(`implementation error`) const lastExtension = nextExtensionsStack[nextExtensionsStack.length - 1] @@ -268,7 +281,7 @@ export const runHook = async ( } } -const createHook = <$X, $F extends (input?: { input?: object; slots?: Record }) => any>( +const createHook = <$X, $F extends (input?: { input?: object; slots?: Slots }) => any>( originalInput: $X, fn: $F, ): $F & { input: $X } => { diff --git a/src/lib/anyware/runPipeline.ts b/src/lib/anyware/runPipeline.ts index 6118de2a6..c8687d866 100644 --- a/src/lib/anyware/runPipeline.ts +++ b/src/lib/anyware/runPipeline.ts @@ -36,6 +36,7 @@ export const runPipeline = async ( originalInput, extensionsStack, asyncErrorDeferred, + customSlots: {}, nextExtensionsStack: [], }) diff --git a/src/lib/anyware/specHelpers.ts b/src/lib/anyware/specHelpers.ts index 77c3b1249..797a76cb9 100644 --- a/src/lib/anyware/specHelpers.ts +++ b/src/lib/anyware/specHelpers.ts @@ -3,29 +3,51 @@ import { beforeEach, vi } from 'vitest' import { Anyware } from './__.js' import { type ExtensionInput, type Options } from './main.js' -export type Input = { value: string } -export const initialInput: Input = { value: `initial` } +export type Input = { + input: { value: string } + slots: { append: (hookName: string) => string } +} + +export const initialInput: Input['input'] = { value: `initial` } // export type $Core = Core<['a', 'b'],Anyware.HookMap<['a','b']>,Input> type $Core = ReturnType & { hooks: { - a: { run: Mock; slots: {} } // eslint-disable-line - b: { run: Mock; slots: {} } // eslint-disable-line + a: { + run: Mock + slots: { + append: Mock<[hookName: string], string> + } + } + b: { + run: Mock + slots: { + append: Mock<[hookName: string], string> + } + } } } export const createAnyware = () => { const a = { - slots: {}, - run: vi.fn().mockImplementation((input: Input) => { - return { value: input.value + `+a` } + slots: { + append: vi.fn().mockImplementation((hookName: string) => { + return hookName + }), + }, + run: vi.fn().mockImplementation(({ input, slots }: Input) => { + return { value: input.value + `+` + slots.append(`a`) } }), } const b = { - slots: {}, - run: vi.fn().mockImplementation((input: Input) => { - return { value: input.value + `+b` } + slots: { + append: vi.fn().mockImplementation((hookName: string) => { + return hookName + }), + }, + run: vi.fn().mockImplementation(({ input, slots }: Input) => { + return { value: input.value + `+` + slots.append(`b`) } }), } From aa3416ce0a40f9e05034567f2780b3e17332953a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 12:28:59 -0400 Subject: [PATCH 05/11] using not slots --- src/lib/anyware/main.test.ts | 2 +- src/lib/anyware/runHook.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/anyware/main.test.ts b/src/lib/anyware/main.test.ts index 131c2bfac..821d0dd9f 100644 --- a/src/lib/anyware/main.test.ts +++ b/src/lib/anyware/main.test.ts @@ -318,7 +318,7 @@ describe('slots', () => { }) test('extension can provide own function to slot on just one of a set of hooks', async () => { const result = await run(async ({ a }) => { - return a({ slots: { append: () => 'x' } }) + return a({ using: { append: () => 'x' } }) }) expect(core.hooks.a.slots.append).not.toBeCalled() expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) diff --git a/src/lib/anyware/runHook.ts b/src/lib/anyware/runHook.ts index 22b8bfea0..fa44a5a1c 100644 --- a/src/lib/anyware/runHook.ts +++ b/src/lib/anyware/runHook.ts @@ -96,7 +96,7 @@ export const runHook = async ( const inputResolved = extensionInput?.input ?? originalInput const customSlotsResolved = { ...customSlots, - ...extensionInput?.slots, + ...extensionInput?.using, } // [1] @@ -281,7 +281,7 @@ export const runHook = async ( } } -const createHook = <$X, $F extends (input?: { input?: object; slots?: Slots }) => any>( +const createHook = <$X, $F extends (input?: HookInput) => any>( originalInput: $X, fn: $F, ): $F & { input: $X } => { @@ -290,3 +290,8 @@ const createHook = <$X, $F extends (input?: { input?: object; slots?: Slots }) = // @ts-expect-error return fn } + +type HookInput = { + input?: object + using?: Slots +} From 80052491280953f00bffea8ac8b4e9dcb108383b Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 12:30:43 -0400 Subject: [PATCH 06/11] deps --- package.json | 16 ++-- pnpm-lock.yaml | 202 +++++++++++++++---------------------------------- 2 files changed, 71 insertions(+), 147 deletions(-) diff --git a/package.json b/package.json index 1e86d2528..ca881f172 100644 --- a/package.json +++ b/package.json @@ -104,21 +104,21 @@ }, "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", - "@pothos/core": "^3.41.1", - "@pothos/plugin-simple-objects": "^3.7.0", + "@pothos/core": "^3.41.2", + "@pothos/plugin-simple-objects": "^3.7.1", "@tsconfig/node18": "^18.2.4", "@tsconfig/strictest": "^2.0.5", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", "@types/json-bigint": "^1.0.4", - "@types/node": "^20.14.7", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", + "@types/node": "^20.14.9", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", "apollo-server-express": "^3.13.0", "body-parser": "^1.20.2", "doctoc": "^2.2.1", "dripip": "^0.10.0", - "eslint": "^9.5.0", + "eslint": "^9.6.0", "eslint-config-prisma": "^0.6.0", "eslint-plugin-deprecation": "^3.0.0", "eslint-plugin-only-warn": "^1.1.0", @@ -135,10 +135,10 @@ "jsdom": "^24.1.0", "json-bigint": "^1.0.0", "publint": "^0.2.8", - "tsx": "^4.15.7", + "tsx": "^4.16.0", "type-fest": "^4.20.1", "typescript": "^5.5.2", - "typescript-eslint": "^7.13.1", + "typescript-eslint": "^7.14.1", "vitest": "^1.6.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 393f6c7e5..4f836db69 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,10 +34,10 @@ importers: specifier: ^0.15.3 version: 0.15.3 '@pothos/core': - specifier: ^3.41.1 + specifier: ^3.41.2 version: 3.41.2(graphql@16.9.0) '@pothos/plugin-simple-objects': - specifier: ^3.7.0 + specifier: ^3.7.1 version: 3.7.1(@pothos/core@3.41.2(graphql@16.9.0))(graphql@16.9.0) '@tsconfig/node18': specifier: ^18.2.4 @@ -55,13 +55,13 @@ importers: specifier: ^1.0.4 version: 1.0.4 '@types/node': - specifier: ^20.14.7 - version: 20.14.7 + specifier: ^20.14.9 + version: 20.14.9 '@typescript-eslint/eslint-plugin': - specifier: ^7.13.1 + specifier: ^7.14.1 version: 7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2) '@typescript-eslint/parser': - specifier: ^7.13.1 + specifier: ^7.14.1 version: 7.14.1(eslint@9.6.0)(typescript@5.5.2) apollo-server-express: specifier: ^3.13.0 @@ -76,7 +76,7 @@ importers: specifier: ^0.10.0 version: 0.10.0 eslint: - specifier: ^9.5.0 + specifier: ^9.6.0 version: 9.6.0 eslint-config-prisma: specifier: ^0.6.0 @@ -127,8 +127,8 @@ importers: specifier: ^0.2.8 version: 0.2.8 tsx: - specifier: ^4.15.7 - version: 4.15.8 + specifier: ^4.16.0 + version: 4.16.0 type-fest: specifier: ^4.20.1 version: 4.20.1 @@ -136,11 +136,11 @@ importers: specifier: ^5.5.2 version: 5.5.2 typescript-eslint: - specifier: ^7.13.1 + specifier: ^7.14.1 version: 7.14.1(eslint@9.6.0)(typescript@5.5.2) vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.14.7)(happy-dom@14.12.3)(jsdom@24.1.0) + version: 1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(jsdom@24.1.0) packages: @@ -411,10 +411,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.10.1': - resolution: {integrity: sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.11.0': resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -427,10 +423,6 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.5.0': - resolution: {integrity: sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.6.0': resolution: {integrity: sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -827,8 +819,8 @@ packages: '@types/node@10.17.60': resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} - '@types/node@20.14.7': - resolution: {integrity: sha512-uTr2m2IbJJucF3KUxgnGOZvYbN0QgkGyWxG6973HCpMYFy2KfcgYuIwkJQMQkt1VbBMlvWRbpshFTLxnxCZjKQ==} + '@types/node@20.14.9': + resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==} '@types/parse-git-config@3.0.4': resolution: {integrity: sha512-jz5eGdk9lBgAd4rMbXTP7MRG7AsGQ8DrXsRumDcXDLClHcpKluislylPVMP/qp90J/LlIrrPZRZIQUflHfrDnQ==} @@ -903,10 +895,6 @@ packages: resolution: {integrity: sha512-5RFjdA/ain/MDUHYXdF173btOKncIrLuBmA9s6FJhzDrRAyVSA+70BHg0/MW6TE+UiKVyRtX91XpVS0gVNwVDQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/scope-manager@7.13.1': - resolution: {integrity: sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/scope-manager@7.14.1': resolution: {integrity: sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -925,10 +913,6 @@ packages: resolution: {integrity: sha512-dU/pKBUpehdEqYuvkojmlv0FtHuZnLXFBn16zsDmlFF3LXkOpkAQ2vrKc3BidIIve9EMH2zfTlxqw9XM0fFN5w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/types@7.13.1': - resolution: {integrity: sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/types@7.14.1': resolution: {integrity: sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -942,15 +926,6 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@7.13.1': - resolution: {integrity: sha512-uxNr51CMV7npU1BxZzYjoVz9iyjckBduFBP0S5sLlh1tXYzHzgZ3BR9SVsNed+LmwKrmnqN3Kdl5t7eZ5TS1Yw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - '@typescript-eslint/typescript-estree@7.14.1': resolution: {integrity: sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -960,12 +935,6 @@ packages: typescript: optional: true - '@typescript-eslint/utils@7.13.1': - resolution: {integrity: sha512-h5MzFBD5a/Gh/fvNdp9pTfqJAbuQC4sCN2WzuXme71lqFJsZtLbjxfSk4r3p02WIArOF9N94pdsLiGutpDbrXQ==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - '@typescript-eslint/utils@7.14.1': resolution: {integrity: sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==} engines: {node: ^18.18.0 || >=20.0.0} @@ -976,10 +945,6 @@ packages: resolution: {integrity: sha512-yRyd2++o/IrJdyHuYMxyFyBhU762MRHQ/bAGQeTnN3pGikfh+nEmM61XTqaDH1XDp53afZ+waXrk0ZvenoZ6xw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@typescript-eslint/visitor-keys@7.13.1': - resolution: {integrity: sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==} - engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/visitor-keys@7.14.1': resolution: {integrity: sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2017,8 +1982,8 @@ packages: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} - https-proxy-agent@7.0.4: - resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} engines: {node: '>= 14'} human-signals@5.0.0: @@ -2205,8 +2170,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-git@1.25.10: - resolution: {integrity: sha512-IxGiaKBwAdcgBXwIcxJU6rHLk+NrzYaaPKXXQffcA0GW3IUrQXdUPDXDo+hkGVcYruuz/7JlGBiuaeTCgIgivQ==} + isomorphic-git@1.26.3: + resolution: {integrity: sha512-NVicJz3RvUhllSSZnCVtTOqWhxlMln5OD3mh00334wCYhiMDuMiYsNJqs3sCHL7oXiv1tP93jMaQTdN7DkPCOg==} engines: {node: '>=12'} hasBin: true @@ -2548,8 +2513,9 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} @@ -2616,9 +2582,9 @@ packages: resolution: {integrity: sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==} engines: {node: '>=8'} - parse-github-url@1.0.2: - resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==} - engines: {node: '>=0.10.0'} + parse-github-url@1.0.3: + resolution: {integrity: sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==} + engines: {node: '>= 0.10'} hasBin: true parse-json@4.0.0: @@ -3126,8 +3092,8 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' - tsx@4.15.8: - resolution: {integrity: sha512-B8dMlbbkZPW0GQ7wafyy2TGXyoGYW0IURfWkM1h/WzgG5lxxRoeDU2VbMURmmjwGaCsoKROVTLmQQPe/s2TnLw==} + tsx@4.16.0: + resolution: {integrity: sha512-MPgN+CuY+4iKxGoJNPv+1pyo5YWZAQ5XfsyobUG+zoKG7IkvCPLZDEyoIb8yLS2FcWci1nlxAqmvPlFWD5AFiQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -3300,8 +3266,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite@5.3.1: - resolution: {integrity: sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==} + vite@5.3.2: + resolution: {integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -3450,8 +3416,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + yocto-queue@1.1.0: + resolution: {integrity: sha512-cMojmlnwkAgIXqga+2sXshlgrrcI0QEPJ5n58pEvtuFo4PaekfomlCudArDD7hj8Hkswjl0/x4eu4q+Xa0WFgQ==} engines: {node: '>=12.20'} zod@3.23.8: @@ -3672,8 +3638,6 @@ snapshots: eslint: 9.6.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.10.1': {} - '@eslint-community/regexpp@4.11.0': {} '@eslint/config-array@0.17.0': @@ -3698,8 +3662,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.5.0': {} - '@eslint/js@9.6.0': {} '@eslint/object-schema@2.1.4': {} @@ -4107,21 +4069,21 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/body-parser@1.19.2': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/connect@3.4.38': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/cors@2.8.12': {} @@ -4144,13 +4106,13 @@ snapshots: '@types/express-serve-static-core@4.17.31': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/express-serve-static-core@4.19.5': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -4189,7 +4151,7 @@ snapshots: '@types/node@10.17.60': {} - '@types/node@20.14.7': + '@types/node@20.14.9': dependencies: undici-types: 5.26.5 @@ -4197,7 +4159,7 @@ snapshots: '@types/parse-github-url@1.0.3': dependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/qs@6.9.15': {} @@ -4206,12 +4168,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.14.7 + '@types/node': 20.14.9 '@types/send': 0.17.4 '@types/unist@2.0.10': {} @@ -4235,7 +4197,7 @@ snapshots: '@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2)': dependencies: - '@eslint-community/regexpp': 4.10.1 + '@eslint-community/regexpp': 4.11.0 '@typescript-eslint/parser': 7.14.1(eslint@9.6.0)(typescript@5.5.2) '@typescript-eslint/scope-manager': 7.14.1 '@typescript-eslint/type-utils': 7.14.1(eslint@9.6.0)(typescript@5.5.2) @@ -4294,11 +4256,6 @@ snapshots: '@typescript-eslint/types': 5.0.0 '@typescript-eslint/visitor-keys': 5.0.0 - '@typescript-eslint/scope-manager@7.13.1': - dependencies: - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/visitor-keys': 7.13.1 - '@typescript-eslint/scope-manager@7.14.1': dependencies: '@typescript-eslint/types': 7.14.1 @@ -4318,8 +4275,6 @@ snapshots: '@typescript-eslint/types@5.0.0': {} - '@typescript-eslint/types@7.13.1': {} - '@typescript-eslint/types@7.14.1': {} '@typescript-eslint/typescript-estree@5.0.0(typescript@4.4.3)': @@ -4336,21 +4291,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@7.13.1(typescript@5.5.2)': - dependencies: - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/visitor-keys': 7.13.1 - debug: 4.3.5 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.2 - ts-api-utils: 1.3.0(typescript@5.5.2) - optionalDependencies: - typescript: 5.5.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@7.14.1(typescript@5.5.2)': dependencies: '@typescript-eslint/types': 7.14.1 @@ -4366,17 +4306,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.13.1(eslint@9.6.0)(typescript@5.5.2)': - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) - '@typescript-eslint/scope-manager': 7.13.1 - '@typescript-eslint/types': 7.13.1 - '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.5.2) - eslint: 9.6.0 - transitivePeerDependencies: - - supports-color - - typescript - '@typescript-eslint/utils@7.14.1(eslint@9.6.0)(typescript@5.5.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.6.0) @@ -4393,11 +4322,6 @@ snapshots: '@typescript-eslint/types': 5.0.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@7.13.1': - dependencies: - '@typescript-eslint/types': 7.13.1 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@7.14.1': dependencies: '@typescript-eslint/types': 7.14.1 @@ -4939,9 +4863,9 @@ snapshots: common-tags: 1.8.2 debug: 4.3.5 fs-jetpack: 3.2.0 - isomorphic-git: 1.25.10 + isomorphic-git: 1.26.3 parse-git-config: 3.0.0 - parse-github-url: 1.0.2 + parse-github-url: 1.0.3 request: 2.88.2 simple-git: 2.48.0 transitivePeerDependencies: @@ -5006,7 +4930,7 @@ snapshots: is-string: 1.0.7 is-typed-array: 1.1.13 is-weakref: 1.0.2 - object-inspect: 1.13.1 + object-inspect: 1.13.2 object-keys: 1.1.1 object.assign: 4.1.5 regexp.prototype.flags: 1.5.2 @@ -5089,12 +5013,12 @@ snapshots: eslint-config-prisma@0.6.0(@typescript-eslint/eslint-plugin@7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2))(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint-plugin-deprecation@3.0.0(eslint@9.6.0)(typescript@5.5.2))(eslint-plugin-only-warn@1.1.0)(eslint-plugin-prefer-arrow@1.2.3(eslint@9.6.0))(eslint-plugin-tsdoc@0.3.0)(eslint@9.6.0)(typescript@5.5.2): dependencies: - '@eslint/js': 9.5.0 + '@eslint/js': 9.6.0 '@types/eslint-config-prettier': 6.11.3 '@types/eslint__js': 8.42.3 '@typescript-eslint/eslint-plugin': 7.14.1(@typescript-eslint/parser@7.14.1(eslint@9.6.0)(typescript@5.5.2))(eslint@9.6.0)(typescript@5.5.2) '@typescript-eslint/parser': 7.14.1(eslint@9.6.0)(typescript@5.5.2) - '@typescript-eslint/utils': 7.13.1(eslint@9.6.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.14.1(eslint@9.6.0)(typescript@5.5.2) eslint: 9.6.0 eslint-config-prettier: 9.1.0(eslint@9.6.0) eslint-plugin-deprecation: 3.0.0(eslint@9.6.0)(typescript@5.5.2) @@ -5146,7 +5070,7 @@ snapshots: eslint-plugin-deprecation@3.0.0(eslint@9.6.0)(typescript@5.5.2): dependencies: - '@typescript-eslint/utils': 7.13.1(eslint@9.6.0)(typescript@5.5.2) + '@typescript-eslint/utils': 7.14.1(eslint@9.6.0)(typescript@5.5.2) eslint: 9.6.0 ts-api-utils: 1.3.0(typescript@5.5.2) tslib: 2.6.3 @@ -5693,7 +5617,7 @@ snapshots: jsprim: 1.4.2 sshpk: 1.18.0 - https-proxy-agent@7.0.4: + https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 debug: 4.3.5 @@ -5850,7 +5774,7 @@ snapshots: isexe@2.0.0: {} - isomorphic-git@1.25.10: + isomorphic-git@1.26.3: dependencies: async-lock: 1.4.1 clean-git-ref: 2.0.1 @@ -5886,7 +5810,7 @@ snapshots: form-data: 4.0.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.4 + https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.10 parse5: 7.1.2 @@ -6249,7 +6173,7 @@ snapshots: object-assign@4.1.1: {} - object-inspect@1.13.1: {} + object-inspect@1.13.2: {} object-keys@1.1.1: {} @@ -6297,7 +6221,7 @@ snapshots: p-limit@5.0.0: dependencies: - yocto-queue: 1.0.0 + yocto-queue: 1.1.0 p-locate@2.0.0: dependencies: @@ -6329,7 +6253,7 @@ snapshots: git-config-path: 2.0.0 ini: 1.3.8 - parse-github-url@1.0.2: {} + parse-github-url@1.0.3: {} parse-json@4.0.0: dependencies: @@ -6676,7 +6600,7 @@ snapshots: call-bind: 1.0.7 es-errors: 1.3.0 get-intrinsic: 1.2.4 - object-inspect: 1.13.1 + object-inspect: 1.13.2 siginfo@2.0.0: {} @@ -6868,7 +6792,7 @@ snapshots: tslib: 1.14.1 typescript: 4.4.3 - tsx@4.15.8: + tsx@4.16.0: dependencies: esbuild: 0.21.5 get-tsconfig: 4.7.5 @@ -7046,13 +6970,13 @@ snapshots: unist-util-stringify-position: 2.0.3 vfile-message: 2.0.4 - vite-node@1.6.0(@types/node@20.14.7): + vite-node@1.6.0(@types/node@20.14.9): dependencies: cac: 6.7.14 debug: 4.3.5 pathe: 1.1.2 picocolors: 1.0.1 - vite: 5.3.1(@types/node@20.14.7) + vite: 5.3.2(@types/node@20.14.9) transitivePeerDependencies: - '@types/node' - less @@ -7063,16 +6987,16 @@ snapshots: - supports-color - terser - vite@5.3.1(@types/node@20.14.7): + vite@5.3.2(@types/node@20.14.9): dependencies: esbuild: 0.21.5 postcss: 8.4.38 rollup: 4.18.0 optionalDependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 fsevents: 2.3.3 - vitest@1.6.0(@types/node@20.14.7)(happy-dom@14.12.3)(jsdom@24.1.0): + vitest@1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(jsdom@24.1.0): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -7091,11 +7015,11 @@ snapshots: strip-literal: 2.1.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.3.1(@types/node@20.14.7) - vite-node: 1.6.0(@types/node@20.14.7) + vite: 5.3.2(@types/node@20.14.9) + vite-node: 1.6.0(@types/node@20.14.9) why-is-node-running: 2.2.2 optionalDependencies: - '@types/node': 20.14.7 + '@types/node': 20.14.9 happy-dom: 14.12.3 jsdom: 24.1.0 transitivePeerDependencies: @@ -7193,7 +7117,7 @@ snapshots: yocto-queue@0.1.0: {} - yocto-queue@1.0.0: {} + yocto-queue@1.1.0: {} zod@3.23.8: {} From 51e071367b427ba2d02a5e41c5051f9e74cbae4e Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 13:25:21 -0400 Subject: [PATCH 07/11] improve upload test --- src/layers/3_SelectionSet/types.test-d.ts | 2 +- src/layers/5_client/client.document.test.ts | 10 ++++---- src/layers/5_client/client.extend.test.ts | 2 +- src/layers/5_client/client.ts | 2 +- src/layers/5_core/core.ts | 7 ++++++ src/layers/6_extensions/Upload/Upload.spec.ts | 23 +++++++++++++++---- src/layers/6_extensions/Upload/Upload.ts | 18 ++------------- .../6_extensions/Upload/extractFiles.ts | 5 +++- src/lib/anyware/__.test-d.ts | 14 ++++++----- src/lib/anyware/main.ts | 12 ++++++---- 10 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/layers/3_SelectionSet/types.test-d.ts b/src/layers/3_SelectionSet/types.test-d.ts index dd670977d..2f13727a5 100644 --- a/src/layers/3_SelectionSet/types.test-d.ts +++ b/src/layers/3_SelectionSet/types.test-d.ts @@ -48,7 +48,7 @@ test(`Query`, () => { assertType({ objectNonNull: { id: true } }) // @ts-expect-error excess property check assertType({ id2: true }) - // @ts-expect-error excess property check + // @ts-expect-error no a2 assertType({ object: { a2: true } }) // Union diff --git a/src/layers/5_client/client.document.test.ts b/src/layers/5_client/client.document.test.ts index a86ddb7ea..461a2d249 100644 --- a/src/layers/5_client/client.document.test.ts +++ b/src/layers/5_client/client.document.test.ts @@ -14,15 +14,17 @@ describe(`document with two queries`, () => { }) // todo allow sync extensions - // eslint-disable-next-line + graffle.use(async ({ exchange }) => { if (exchange.input.transport !== `http`) return exchange() // @ts-expect-error Nextjs exchange.input.request.next = { revalidate: 60, tags: [`menu`] } return exchange({ - ...exchange.input, - request: { - ...exchange.input.request, + input: { + ...exchange.input, + request: { + ...exchange.input.request, + }, }, }) }).document({ diff --git a/src/layers/5_client/client.extend.test.ts b/src/layers/5_client/client.extend.test.ts index 9485cb37a..0b10d8e43 100644 --- a/src/layers/5_client/client.extend.test.ts +++ b/src/layers/5_client/client.extend.test.ts @@ -32,7 +32,7 @@ describe(`entrypoint request`, () => { }) const client2 = client.use(async ({ pack }) => { const { exchange } = await pack({ input: { ...pack.input, headers } }) - return await exchange(exchange.input) + return await exchange({ input: exchange.input }) }) expect(await client2.query.id()).toEqual(db.id) }) diff --git a/src/layers/5_client/client.ts b/src/layers/5_client/client.ts index ac2345e18..64aff7b45 100644 --- a/src/layers/5_client/client.ts +++ b/src/layers/5_client/client.ts @@ -70,7 +70,7 @@ export type Client<$Index extends Schema.Index | null, $Config extends Config> = : {} // eslint-disable-line ) & { - use: (extension: Extension) => Client<$Index, $Config> + use: (extension: Extension | Anyware.Extension2) => Client<$Index, $Config> retry: (extension: Anyware.Extension2) => Client<$Index, $Config> } diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index 8aedd3336..9e89f8242 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -264,8 +264,15 @@ export const anyware = Anyware.create({ unpack: async ({ input }) => { switch (input.transport) { case `http`: { + // todo if response is missing header of content length then .json() hangs forever. + // todo firstly consider a timeout, secondly, if response is malformed, then don't even run .json() + // console.log(input.response.headers) + // console.log(await input.response.json().then(console.log).catch(console.error)) + // console.log(2) const json = await input.response.json() as object + // console.log(2) const result = parseExecutionResult(json) + // console.log(10) return { ...input, result, diff --git a/src/layers/6_extensions/Upload/Upload.spec.ts b/src/layers/6_extensions/Upload/Upload.spec.ts index ddc320f34..272d90e46 100644 --- a/src/layers/6_extensions/Upload/Upload.spec.ts +++ b/src/layers/6_extensions/Upload/Upload.spec.ts @@ -1,3 +1,4 @@ +import getPort from 'get-port' import { processRequest } from 'graphql-upload-minimal' import type { Server } from 'http' import { createServer } from 'http' @@ -9,8 +10,9 @@ import { execute } from '../../0_functions/execute.js' import { Upload } from './Upload.js' let server: Server +let port: number -beforeAll(() => { +beforeAll(async () => { // eslint-disable-next-line server = createServer(async (request, response) => { const body = await processRequest(request, response) @@ -21,20 +23,33 @@ beforeAll(() => { variables: body.variables as StandardScalarVariables, operationName: body.operationName ?? undefined, }) + response.setHeader(`Content-Type`, `application/json`) + response.setHeader(`content-length`, JSON.stringify(result).length.toString()) response.write(JSON.stringify(result)) response.statusCode = 200 response.statusMessage = `OK` }) - server.listen(3000) + port = await getPort({ port: [3000, 3001, 3002, 3003, 3004] }) + server.listen(port) + await new Promise((resolve) => + server.once(`listening`, () => { + resolve(undefined) + }) + ) }) afterAll(async () => { - await new Promise((resolve) => server.close(resolve)) + await new Promise((resolve) => { + server.close(resolve) + setImmediate(() => { + server.emit(`close`) + }) + }) }) test(`upload`, async () => { const graffle = Graffle.create({ - schema: new URL(`http://localhost:3000`), + schema: new URL(`http://localhost:${String(port)}`), }).use(Upload) const result = await graffle.raw({ diff --git a/src/layers/6_extensions/Upload/Upload.ts b/src/layers/6_extensions/Upload/Upload.ts index 3dd980e1e..388e376f3 100644 --- a/src/layers/6_extensions/Upload/Upload.ts +++ b/src/layers/6_extensions/Upload/Upload.ts @@ -9,7 +9,7 @@ import extractFiles from './extractFiles.js' export const Upload = createExtension({ name: `Upload`, anyware: async ({ encode }) => { - const { pack } = await encode({ + return await encode({ using: { body: (input) => { if (!(input.variables && isUsingUploadScalar(input.variables))) return @@ -24,19 +24,6 @@ export const Upload = createExtension({ }, }, }) - - // const { exchange } = await pack() - // if (exchange.input.transport !== `http`) return exchange() - - // return await exchange({ - // input: { - // ...exchange.input, - // request: { - // ...exchange.input.request, - // headers: {}, - // }, - // }, - // }) }, }) @@ -47,7 +34,6 @@ const createUploadBody = (input: ExecutionInput): FormData => { ``, ) const operationJSON = JSON.stringify(clone) - console.log(clone, files) if (files.size === 0) throw new Error(`Not an upload request.`) @@ -64,7 +50,7 @@ const createUploadBody = (input: ExecutionInput): FormData => { i = 0 for (const file of files.keys()) { - form.append(`${++i}`, file) + form.append(String(++i), file) } return form diff --git a/src/layers/6_extensions/Upload/extractFiles.ts b/src/layers/6_extensions/Upload/extractFiles.ts index 655501317..9b3e0856f 100644 --- a/src/layers/6_extensions/Upload/extractFiles.ts +++ b/src/layers/6_extensions/Upload/extractFiles.ts @@ -58,7 +58,10 @@ import isPlainObject from 'is-plain-obj' * | `file1` | `["prefix.a", "prefix.b.0"]` | * | `file2` | `["prefix.b.1"]` | */ -export default function extractFiles(value: any, isExtractable: any, path = ``) { +export default function extractFiles(value: any, isExtractable: any, path = ``): { + clone: object + files: Map +} { if (!arguments.length) throw new TypeError(`Argument 1 \`value\` is required.`) if (typeof isExtractable !== `function`) { diff --git a/src/lib/anyware/__.test-d.ts b/src/lib/anyware/__.test-d.ts index 6278aeecb..7c5e936e5 100644 --- a/src/lib/anyware/__.test-d.ts +++ b/src/lib/anyware/__.test-d.ts @@ -82,18 +82,20 @@ describe('withSlots', () => { (input: { initialInput: InputA options?: Anyware.Options + extensions: ((input: { + a: SomeHook< + ( + input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }, + ) => MaybePromise + > + }) => Promise)[] retryingExtension?: (input: { a: SomeHook< - (input?: { input?: InputA; using: { x?: (x: boolean) => number | undefined } }) => MaybePromise< + (input?: { input?: InputA; using?: { x?: (x: boolean) => number | undefined } }) => MaybePromise< Error | Result > > }) => Promise - extensions: ((input: { - a: SomeHook< - (input?: { input?: InputA }) => MaybePromise - > - }) => Promise)[] }) => Promise >() }) diff --git a/src/lib/anyware/main.ts b/src/lib/anyware/main.ts index 689ef4eb9..e5de1ef0d 100644 --- a/src/lib/anyware/main.ts +++ b/src/lib/anyware/main.ts @@ -51,12 +51,14 @@ export type SomeHookEnvelope = { [name: string]: SomeHook } -export type SomeHook any = (input: { input: any }) => any> = fn & { +export type SomeHook< + fn extends (input?: { input?: any; using?: any }) => any = (input?: { input?: any; using?: any }) => any, +> = fn & { [hookSymbol]: HookSymbol // todo the result is unknown, but if we build a EndEnvelope, then we can work with this type more logically and put it here. // E.g. adding `| unknown` would destroy the knowledge of hook envelope case // todo this is not strictly true, it could also be the final result - input: Parameters[0]['input'] + input: Exclude[0], undefined>['input'] } export type HookMap<$HookSequence extends HookSequence> = Record< @@ -78,7 +80,7 @@ type Hook< & (<$$Input extends $HookMap[$Name]['input']>( input?: { input?: $$Input - } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), + } & (keyof $HookMap[$Name]['slots'] extends never ? {} : { using?: SlotInputify<$HookMap[$Name]['slots']> }), // eslint-disable-line ) => HookReturn<$HookSequence, $HookMap, $Result, $Name, $Options>) & { [hookSymbol]: HookSymbol @@ -86,7 +88,7 @@ type Hook< } type SlotInputify<$Slots extends Record any>> = { - [K in keyof $Slots]: SlotInput<$Slots[K]> + [K in keyof $Slots]?: SlotInput<$Slots[K]> } type SlotInput any> = (...args: Parameters) => ReturnType | undefined @@ -222,7 +224,7 @@ const createPassthrough = (hookName: string) => async (hookEnvelope: SomeHookEnv if (!hook) { throw new Errors.ContextualError(`Hook not found in hook envelope`, { hookName }) } - return await hook({ input: hook.input, slots: hook.slots }) + return await hook({ input: hook.input }) // eslint-disable-line } type Config = Required From 4382fcd6b2c63d3e60c891d77c41cc2cc651437a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 13:28:11 -0400 Subject: [PATCH 08/11] lint --- src/layers/6_extensions/Upload/Upload.ts | 2 +- src/lib/anyware/runHook.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layers/6_extensions/Upload/Upload.ts b/src/layers/6_extensions/Upload/Upload.ts index 388e376f3..84979ef5f 100644 --- a/src/layers/6_extensions/Upload/Upload.ts +++ b/src/layers/6_extensions/Upload/Upload.ts @@ -56,6 +56,6 @@ const createUploadBody = (input: ExecutionInput): FormData => { return form } -const isUsingUploadScalar = (variables: StandardScalarVariables) => { +const isUsingUploadScalar = (_variables: StandardScalarVariables) => { return true // todo } diff --git a/src/lib/anyware/runHook.ts b/src/lib/anyware/runHook.ts index fa44a5a1c..ad98d7841 100644 --- a/src/lib/anyware/runHook.ts +++ b/src/lib/anyware/runHook.ts @@ -258,10 +258,10 @@ export const runHook = async ( let result try { const slotsResolved = { - ...implementation.slots, + ...implementation.slots as Slots, // todo is this cast needed, can we Slots type the property? ...customSlots, } - result = await implementation.run({ input: originalInput as any, slots: slotsResolved }) + result = await implementation.run({ input: originalInput, slots: slotsResolved }) } catch (error) { debugHook(`implementation error`) const lastExtension = nextExtensionsStack[nextExtensionsStack.length - 1] From 0c7591c02b46b2e00ed100c35920fac02c3d2815 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 23:04:15 -0400 Subject: [PATCH 09/11] more tests --- src/lib/anyware/main.test.ts | 16 +++++++++++++++- src/lib/anyware/specHelpers.ts | 16 +++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/lib/anyware/main.test.ts b/src/lib/anyware/main.test.ts index 821d0dd9f..bbb040a4e 100644 --- a/src/lib/anyware/main.test.ts +++ b/src/lib/anyware/main.test.ts @@ -324,6 +324,20 @@ describe('slots', () => { expect(core.hooks.b.slots.append.mock.calls[0]).toMatchObject(['b']) expect(result).toEqual({ value: 'initial+x+b' }) }) + test('extension can provide own functions to slots on multiple of a set of hooks', async () => { + const result = await run(async ({ a }) => { + return a({ using: { append: () => 'x', appendExtra: () => '+x2' } }) + }) + expect(result).toEqual({ value: 'initial+x+x2+b' }) + }) // todo hook with two slots - // todo two extensions, each using the slot (later one should win) + test('two extensions can each provide own function to same slot on just one of a set of hooks, and the later one wins', async () => { + const result = await run(async ({ a }) => { + const { b } = await a({ using: { append: () => 'x' } }) + return b({ using: { append: () => 'y' } }) + }) + expect(core.hooks.a.slots.append).not.toBeCalled() + expect(core.hooks.b.slots.append).not.toBeCalled() + expect(result).toEqual({ value: 'initial+x+y' }) + }) }) diff --git a/src/lib/anyware/specHelpers.ts b/src/lib/anyware/specHelpers.ts index 797a76cb9..8768710f8 100644 --- a/src/lib/anyware/specHelpers.ts +++ b/src/lib/anyware/specHelpers.ts @@ -5,7 +5,7 @@ import { type ExtensionInput, type Options } from './main.js' export type Input = { input: { value: string } - slots: { append: (hookName: string) => string } + slots: { append: (hookName: string) => string; appendExtra: (hookName: string) => string } } export const initialInput: Input['input'] = { value: `initial` } @@ -18,12 +18,14 @@ type $Core = ReturnType & { run: Mock slots: { append: Mock<[hookName: string], string> + appendExtra: Mock<[hookName: string], string> } } b: { run: Mock slots: { append: Mock<[hookName: string], string> + appendExtra: Mock<[hookName: string], string> } } } @@ -35,9 +37,13 @@ export const createAnyware = () => { append: vi.fn().mockImplementation((hookName: string) => { return hookName }), + appendExtra: vi.fn().mockImplementation(() => { + return `` + }), }, run: vi.fn().mockImplementation(({ input, slots }: Input) => { - return { value: input.value + `+` + slots.append(`a`) } + const extra = slots.appendExtra(`a`) + return { value: input.value + `+` + slots.append(`a`) + extra } }), } const b = { @@ -45,9 +51,13 @@ export const createAnyware = () => { append: vi.fn().mockImplementation((hookName: string) => { return hookName }), + appendExtra: vi.fn().mockImplementation(() => { + return `` + }), }, run: vi.fn().mockImplementation(({ input, slots }: Input) => { - return { value: input.value + `+` + slots.append(`b`) } + const extra = slots.appendExtra(`b`) + return { value: input.value + `+` + slots.append(`b`) + extra } }), } From bc3d8bd9c13c9d5b12d10e95d20888f43d345068 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 23:32:13 -0400 Subject: [PATCH 10/11] final implementation --- package.json | 1 + pnpm-lock.yaml | 207 ++++++++++++++++++ src/layers/5_core/core.ts | 10 +- src/layers/6_extensions/Upload/Upload.spec.ts | 56 ++--- src/layers/6_extensions/Upload/Upload.ts | 2 +- 5 files changed, 241 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index ca881f172..566869256 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "graphql-scalars": "^1.23.0", "graphql-tag": "^2.12.6", "graphql-upload-minimal": "^1.6.1", + "graphql-yoga": "^5.5.0", "jsdom": "^24.1.0", "json-bigint": "^1.0.0", "publint": "^0.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f836db69..7cc8fc50f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,6 +117,9 @@ importers: graphql-upload-minimal: specifier: ^1.6.1 version: 1.6.1(graphql@16.9.0) + graphql-yoga: + specifier: ^5.5.0 + version: 5.5.0(graphql@16.9.0) jsdom: specifier: ^24.1.0 version: 24.1.0 @@ -263,6 +266,14 @@ packages: cpu: [x64] os: [win32] + '@envelop/core@5.0.1': + resolution: {integrity: sha512-wxA8EyE1fPnlbP0nC/SFI7uU8wSNf4YjxZhAPu0P63QbgIvqHtHsH4L3/u+rsTruzhk3OvNRgQyLsMfaR9uzAQ==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.0.0': + resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} + engines: {node: '>=18.0.0'} + '@es-joy/jsdoccomment@0.4.4': resolution: {integrity: sha512-ua4qDt9dQb4qt5OI38eCZcQZYE5Bq3P0GzgvDARdT8Lt0mAUpxKTPy8JGGqEvF77tG1irKDZ3WreeezEa3P43w==} engines: {node: '>=10.0.0'} @@ -431,6 +442,12 @@ packages: resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@graphql-tools/executor@1.2.7': + resolution: {integrity: sha512-oyIw69QA+PuS/g7ttZZeEpIPS5CCGiIYitGtNxaChuiK7NPb7FD1dwOEXyekQt9/2FOEqZoYNpRY0NFfx/tO9Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/merge@8.3.1': resolution: {integrity: sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==} peerDependencies: @@ -441,11 +458,23 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/merge@9.0.4': + resolution: {integrity: sha512-MivbDLUQ+4Q8G/Hp/9V72hbn810IJDEZQ57F01sHnlrrijyadibfVhaQfW/pNH+9T/l8ySZpaR/DpL5i+ruZ+g==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/mock@8.7.20': resolution: {integrity: sha512-ljcHSJWjC/ZyzpXd5cfNhPI7YljRVvabKHPzKjEs5ElxWu2cdlLGvyNYepApXDsM/OJG/2xuhGM+9GWu5gEAPQ==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/schema@10.0.4': + resolution: {integrity: sha512-HuIwqbKxPaJujox25Ra4qwz0uQzlpsaBOzO6CVfzB/MemZdd+Gib8AIvfhQArK0YIN40aDran/yi+E5Xf0mQww==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/schema@8.5.1': resolution: {integrity: sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==} peerDependencies: @@ -456,6 +485,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@10.2.2': + resolution: {integrity: sha512-ueoplzHIgFfxhFrF4Mf/niU/tYHuO6Uekm2nCYU72qpI+7Hn9dA2/o5XOBvFXDk27Lp5VSvQY5WfmRbqwVxaYQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-tools/utils@8.9.0': resolution: {integrity: sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==} peerDependencies: @@ -471,6 +506,18 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-yoga/logger@2.0.0': + resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/subscription@5.0.1': + resolution: {integrity: sha512-1wCB1DfAnaLzS+IdoOzELGGnx1ODEg9nzQXFh4u2j02vAnne6d+v4A7HIH9EqzVdPLoAaMKXCZUUdKs+j3z1fg==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/typed-event-target@3.0.0': + resolution: {integrity: sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==} + engines: {node: '>=18.0.0'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -489,6 +536,9 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@kamilkisiela/fast-url-parser@1.1.4': + resolution: {integrity: sha512-gbkePEBupNydxCelHCESvFSFM8XPh1Zs/OAVRW/rKpEqPAl5PbOM90Si8mv9bvnR53uPD2s/FiRxdvSejpRJew==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -651,6 +701,9 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@repeaterjs/repeater@3.0.6': + resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@rollup/rollup-android-arm-eabi@4.18.0': resolution: {integrity: sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==} cpu: [arm] @@ -964,6 +1017,22 @@ packages: '@vitest/utils@1.6.0': resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + '@whatwg-node/events@0.1.1': + resolution: {integrity: sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/fetch@0.9.18': + resolution: {integrity: sha512-hqoz6StCW+AjV/3N+vg0s1ah82ptdVUb9nH2ttj3UbySOXUvytWw2yqy8c1cKzyRk6mDD00G47qS3fZI9/gMjg==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/node-fetch@0.5.11': + resolution: {integrity: sha512-LS8tSomZa3YHnntpWt3PP43iFEEl6YeIsvDakczHBKlay5LdkXFr8w7v8H6akpG5nRrzydyB0k1iE2eoL6aKIQ==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/server@0.9.36': + resolution: {integrity: sha512-KT9qKLmbuWSuFv0Vg4JyK2vN2+vSuQPeEa25xpndYFROAIZntYe7e2BlWAk9l7IrgnV+M4bCVhjrAwwRsaCeiA==} + engines: {node: '>=16.0.0'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1281,6 +1350,10 @@ packages: engines: {node: '>=0.8'} hasBin: true + cross-inspect@1.0.0: + resolution: {integrity: sha512-4PFfn4b5ZN6FMNGSZlyb7wUhuN8wvj8t/VQHZdM4JsDcruGJ8L2kf9zao98QIrBPFCpdk27qst/AGTl7pL3ypQ==} + engines: {node: '>=16.0.0'} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1414,6 +1487,10 @@ packages: resolution: {integrity: sha512-FaHkW6uXAa57pJwz+SRxTvTDiybSH9w4PGWXkheoIPNs4HcHM688rfsKzvedoaLvQul4UaAoRr+2CHc7V25biA==} hasBin: true + dset@3.1.3: + resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} + engines: {node: '>=4'} + ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} @@ -1718,6 +1795,9 @@ packages: resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} engines: {'0': node >=0.6.0} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1734,6 +1814,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -1913,6 +1996,12 @@ packages: peerDependencies: graphql: 0.13.1 - 16 + graphql-yoga@5.5.0: + resolution: {integrity: sha512-GrF1GfR5RJb/iHEcRQakXSSoqXb2dgGa0UCU0CXsfYnuUstSog6nF5oCUKlXz1UnFbpNAlVhU++rd088S4VAtg==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + graphql@16.9.0: resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -2294,6 +2383,10 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lru-cache@10.3.0: + resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==} + engines: {node: 14 || >=16.14} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -3216,6 +3309,9 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3558,6 +3654,15 @@ snapshots: '@dprint/win32-x64@0.46.2': optional: true + '@envelop/core@5.0.1': + dependencies: + '@envelop/types': 5.0.0 + tslib: 2.6.3 + + '@envelop/types@5.0.0': + dependencies: + tslib: 2.6.3 + '@es-joy/jsdoccomment@0.4.4': dependencies: comment-parser: 1.1.5 @@ -3666,6 +3771,15 @@ snapshots: '@eslint/object-schema@2.1.4': {} + '@graphql-tools/executor@1.2.7(graphql@16.9.0)': + dependencies: + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) + '@repeaterjs/repeater': 3.0.6 + graphql: 16.9.0 + tslib: 2.6.3 + value-or-promise: 1.0.12 + '@graphql-tools/merge@8.3.1(graphql@16.9.0)': dependencies: '@graphql-tools/utils': 8.9.0(graphql@16.9.0) @@ -3678,6 +3792,12 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 + '@graphql-tools/merge@9.0.4(graphql@16.9.0)': + dependencies: + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + graphql: 16.9.0 + tslib: 2.6.3 + '@graphql-tools/mock@8.7.20(graphql@16.9.0)': dependencies: '@graphql-tools/schema': 9.0.19(graphql@16.9.0) @@ -3686,6 +3806,14 @@ snapshots: graphql: 16.9.0 tslib: 2.6.3 + '@graphql-tools/schema@10.0.4(graphql@16.9.0)': + dependencies: + '@graphql-tools/merge': 9.0.4(graphql@16.9.0) + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + graphql: 16.9.0 + tslib: 2.6.3 + value-or-promise: 1.0.12 + '@graphql-tools/schema@8.5.1(graphql@16.9.0)': dependencies: '@graphql-tools/merge': 8.3.1(graphql@16.9.0) @@ -3702,6 +3830,14 @@ snapshots: tslib: 2.6.3 value-or-promise: 1.0.12 + '@graphql-tools/utils@10.2.2(graphql@16.9.0)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) + cross-inspect: 1.0.0 + dset: 3.1.3 + graphql: 16.9.0 + tslib: 2.6.3 + '@graphql-tools/utils@8.9.0(graphql@16.9.0)': dependencies: graphql: 16.9.0 @@ -3717,6 +3853,22 @@ snapshots: dependencies: graphql: 16.9.0 + '@graphql-yoga/logger@2.0.0': + dependencies: + tslib: 2.6.3 + + '@graphql-yoga/subscription@5.0.1': + dependencies: + '@graphql-yoga/typed-event-target': 3.0.0 + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/events': 0.1.1 + tslib: 2.6.3 + + '@graphql-yoga/typed-event-target@3.0.0': + dependencies: + '@repeaterjs/repeater': 3.0.6 + tslib: 2.6.3 + '@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/retry@0.3.0': {} @@ -3729,6 +3881,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.4.15': {} + '@kamilkisiela/fast-url-parser@1.1.4': {} + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.3.5 @@ -3995,6 +4149,8 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@repeaterjs/repeater@3.0.6': {} + '@rollup/rollup-android-arm-eabi@4.18.0': optional: true @@ -4356,6 +4512,26 @@ snapshots: loupe: 2.3.7 pretty-format: 29.7.0 + '@whatwg-node/events@0.1.1': {} + + '@whatwg-node/fetch@0.9.18': + dependencies: + '@whatwg-node/node-fetch': 0.5.11 + urlpattern-polyfill: 10.0.0 + + '@whatwg-node/node-fetch@0.5.11': + dependencies: + '@kamilkisiela/fast-url-parser': 1.1.4 + '@whatwg-node/events': 0.1.1 + busboy: 1.6.0 + fast-querystring: 1.1.2 + tslib: 2.6.3 + + '@whatwg-node/server@0.9.36': + dependencies: + '@whatwg-node/fetch': 0.9.18 + tslib: 2.6.3 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -4715,6 +4891,10 @@ snapshots: crc-32@1.2.2: {} + cross-inspect@1.0.0: + dependencies: + tslib: 2.6.3 + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -4872,6 +5052,8 @@ snapshots: - encoding - supports-color + dset@3.1.3: {} + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 @@ -5334,6 +5516,8 @@ snapshots: extsprintf@1.3.0: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -5350,6 +5534,10 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -5545,6 +5733,21 @@ snapshots: busboy: 1.6.0 graphql: 16.9.0 + graphql-yoga@5.5.0(graphql@16.9.0): + dependencies: + '@envelop/core': 5.0.1 + '@graphql-tools/executor': 1.2.7(graphql@16.9.0) + '@graphql-tools/schema': 10.0.4(graphql@16.9.0) + '@graphql-tools/utils': 10.2.2(graphql@16.9.0) + '@graphql-yoga/logger': 2.0.0 + '@graphql-yoga/subscription': 5.0.1 + '@whatwg-node/fetch': 0.9.18 + '@whatwg-node/server': 0.9.36 + dset: 3.1.3 + graphql: 16.9.0 + lru-cache: 10.3.0 + tslib: 2.6.3 + graphql@16.9.0: {} happy-dom@14.12.3: @@ -5915,6 +6118,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + lru-cache@10.3.0: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -6931,6 +7136,8 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + urlpattern-polyfill@10.0.0: {} + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index 9e89f8242..2465705fb 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -264,15 +264,11 @@ export const anyware = Anyware.create({ unpack: async ({ input }) => { switch (input.transport) { case `http`: { - // todo if response is missing header of content length then .json() hangs forever. - // todo firstly consider a timeout, secondly, if response is malformed, then don't even run .json() - // console.log(input.response.headers) - // console.log(await input.response.json().then(console.log).catch(console.error)) - // console.log(2) + // todo 1 if response is missing header of content length then .json() hangs forever. + // todo 1 firstly consider a timeout, secondly, if response is malformed, then don't even run .json() + // todo 2 if response is e.g. 404 with no json body, then an error is thrown because json parse cannot work, not gracefully handled here const json = await input.response.json() as object - // console.log(2) const result = parseExecutionResult(json) - // console.log(10) return { ...input, result, diff --git a/src/layers/6_extensions/Upload/Upload.spec.ts b/src/layers/6_extensions/Upload/Upload.spec.ts index 272d90e46..cf672f904 100644 --- a/src/layers/6_extensions/Upload/Upload.spec.ts +++ b/src/layers/6_extensions/Upload/Upload.spec.ts @@ -1,34 +1,21 @@ import getPort from 'get-port' -import { processRequest } from 'graphql-upload-minimal' -import type { Server } from 'http' -import { createServer } from 'http' -import { afterAll, beforeAll, test } from 'vitest' +import type { Server } from 'node:http' +import { createServer } from 'node:http' +import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest' import { schema } from '../../../../tests/_/schemaUpload/schema.js' import { Graffle } from '../../../entrypoints/alpha/main.js' -import type { StandardScalarVariables } from '../../../lib/graphql.js' -import { execute } from '../../0_functions/execute.js' import { Upload } from './Upload.js' +import { createYoga } from 'graphql-yoga' +import type { Client } from '../../5_client/client.js' + let server: Server let port: number +let graffle: Client beforeAll(async () => { - // eslint-disable-next-line - server = createServer(async (request, response) => { - const body = await processRequest(request, response) - if (Array.isArray(body)) throw new Error(`Batch requests not supported.`) - const result = await execute({ - schema: schema, - document: body.query, - variables: body.variables as StandardScalarVariables, - operationName: body.operationName ?? undefined, - }) - response.setHeader(`Content-Type`, `application/json`) - response.setHeader(`content-length`, JSON.stringify(result).length.toString()) - response.write(JSON.stringify(result)) - response.statusCode = 200 - response.statusMessage = `OK` - }) + const yoga = createYoga({ schema }) + server = createServer(yoga) // eslint-disable-line port = await getPort({ port: [3000, 3001, 3002, 3003, 3004] }) server.listen(port) await new Promise((resolve) => @@ -38,6 +25,12 @@ beforeAll(async () => { ) }) +beforeEach(() => { + graffle = Graffle.create({ + schema: new URL(`http://localhost:${String(port)}/graphql`), + }).use(Upload) +}) + afterAll(async () => { await new Promise((resolve) => { server.close(resolve) @@ -48,10 +41,6 @@ afterAll(async () => { }) test(`upload`, async () => { - const graffle = Graffle.create({ - schema: new URL(`http://localhost:${String(port)}`), - }).use(Upload) - const result = await graffle.raw({ document: ` mutation ($blob: Upload!) { @@ -62,5 +51,18 @@ test(`upload`, async () => { blob: new Blob([`Hello World`], { type: `text/plain` }) as any, // eslint-disable-line }, }) - console.log(result) + expect(result).toMatchInlineSnapshot(` + { + "data": { + "readTextFile": "Hello World", + }, + "errors": undefined, + "extensions": undefined, + } + `) }) + +// todo test that non-upload requests work + +// todo test with non-raw +// ^ for this to work we need to generate documents that use variables diff --git a/src/layers/6_extensions/Upload/Upload.ts b/src/layers/6_extensions/Upload/Upload.ts index 84979ef5f..39d5d6004 100644 --- a/src/layers/6_extensions/Upload/Upload.ts +++ b/src/layers/6_extensions/Upload/Upload.ts @@ -57,5 +57,5 @@ const createUploadBody = (input: ExecutionInput): FormData => { } const isUsingUploadScalar = (_variables: StandardScalarVariables) => { - return true // todo + return Object.values(_variables).some(_ => _ instanceof Blob) } From f3d0178043b12fbf86798f3ecc25a36f968a6b61 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sat, 29 Jun 2024 23:47:20 -0400 Subject: [PATCH 11/11] no run test in jsdom --- src/layers/6_extensions/Upload/Upload.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/layers/6_extensions/Upload/Upload.spec.ts b/src/layers/6_extensions/Upload/Upload.spec.ts index cf672f904..2c4ee4fa5 100644 --- a/src/layers/6_extensions/Upload/Upload.spec.ts +++ b/src/layers/6_extensions/Upload/Upload.spec.ts @@ -1,3 +1,6 @@ +// todo in order to test jsdom, we need to boot the server in a separate process +// @vitest-environment node + import getPort from 'get-port' import type { Server } from 'node:http' import { createServer } from 'node:http'