Skip to content

Commit

Permalink
improve(request-pipeline): no conditional type on config (#1260)
Browse files Browse the repository at this point in the history
This change removes some ergonomics from the request pipeline anyware in
order to pursue the larger modular request pipeline refactor. We should
be able to bring back even better DX but with a very different approach
by the end.
  • Loading branch information
jasonkuhrt authored Nov 12, 2024
1 parent c92a67b commit aad4cac
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ const graffle = Graffle
.create({
schema: publicGraphQLSchemaEndpoints.Pokemon,
})
.anyware(async ({ pack }) => {
return await pack({
.anyware(({ pack }) => {
if (pack.input.transportType !== `http`) return pack()
return pack({
input: {
...pack.input,
headers: {
Expand All @@ -19,7 +20,7 @@ const graffle = Graffle
},
})
})
.anyware(async ({ exchange }) => {
.anyware(({ exchange }) => {
// todo wrong type / runtime value
show(exchange.input.request)
return exchange()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const graffle = Graffle
.with({
transport: { headers: { 'x-something-to-unset': `` } },
})
.anyware(async ({ exchange }) => {
.anyware(({ exchange }) => {
if (exchange.input.transportType !== `http`) return exchange()
show(exchange.input.request.headers)
return exchange()
})
Expand Down
2 changes: 2 additions & 0 deletions examples/50_anyware/anyware_jump-start__jump-start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Graffle
// Notice how we **start** with the `exchange` hook, skipping the `encode` and `pack` hooks.
.anyware(async ({ exchange }) => {
// ^^^^^^^^
if (exchange.input.transportType !== `http`) return exchange()

const mergedHeaders = new Headers(exchange.input.request.headers)
mergedHeaders.set(`X-Custom-Header`, `123`)

Expand Down
3 changes: 3 additions & 0 deletions examples/50_anyware/anyware_short-circuit__short-circuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Graffle
.anyware(async ({ encode }) => {
const { pack } = await encode()
const { exchange } = await pack()

if (exchange.input.transportType !== `http`) return exchange()

const mergedHeaders = new Headers(exchange.input.request.headers)
mergedHeaders.set(`X-Custom-Header`, `123`)
// Notice how we **end** with the `exchange` hook, skipping the `unpack` and `decode` hooks.
Expand Down
2 changes: 1 addition & 1 deletion src/client/builderExtensions/anyware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface Anyware<$Arguments extends Builder.Extension.Parameters<Builder
*/
anyware: (
interceptor: AnywareLib.Interceptor.InferConstructor<
requestPipeline.Spec<$Arguments['context']['config']>
requestPipeline.Spec
>,
) => Builder.Definition.MaterializeWithNewContext<$Arguments['chain'], $Arguments['context']>
}
Expand Down
6 changes: 4 additions & 2 deletions src/client/builderExtensions/use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ describe(`entrypoint pack`, () => {
return createResponse({ data: { id: db.id } })
})
const client2 = client.anyware(async ({ pack }) => {
return await pack({ input: { ...pack.input, headers } })
if (pack.input.transportType !== `http`) return pack()
return pack({ input: { ...pack.input, headers } })
})
expect(await client2.query.id()).toEqual(db.id)
})
Expand All @@ -51,8 +52,9 @@ describe(`entrypoint pack`, () => {
return createResponse({ data: { id: db.id } })
})
const client2 = client.anyware(async ({ pack }) => {
if (pack.input.transportType !== `http`) return pack()
const { exchange } = await pack({ input: { ...pack.input, headers } })
return await exchange({ input: exchange.input })
return exchange({ input: exchange.input })
})
expect(await client2.query.id()).toEqual(db.id)
})
Expand Down
52 changes: 26 additions & 26 deletions src/client/client.transport-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,35 @@ import { Graffle as Pokemon } from '../../tests/_/schemas/pokemon/graffle/__.js'
import { schema as schemaPokemon } from '../../tests/_/schemas/pokemon/schema.js'
import { Graffle } from '../entrypoints/main.js'
import { ACCEPT_REC, CONTENT_TYPE_REC } from '../lib/grafaid/http/http.js'
import type { requestPipeline } from '../requestPipeline/__.js'
import { Transport, type TransportHttp } from '../types/Transport.js'
// import type { requestPipeline } from '../requestPipeline/__.js'
// import { Transport, type TransportHttp } from '../types/Transport.js'

const schema = new URL(`https://foo.io/api/graphql`)

test(`anyware hooks are typed to http transport`, () => {
Graffle.create({ schema }).anyware(async ({ encode }) => {
expectTypeOf(encode.input.transportType).toEqualTypeOf<TransportHttp>()
const { pack } = await encode()
expectTypeOf(pack.input.transportType).toEqualTypeOf(Transport.http)
const { exchange } = await pack()
expectTypeOf(exchange.input.transportType).toEqualTypeOf(Transport.http)
// todo we can statically track the method mode like we do the transport mode
expectTypeOf(exchange.input.request).toEqualTypeOf<
requestPipeline.Steps.CoreExchangePostRequest | requestPipeline.Steps.CoreExchangeGetRequest
>()
const { unpack } = await exchange()
expectTypeOf(unpack.input.transportType).toEqualTypeOf(Transport.http)
expectTypeOf(unpack.input.response).toEqualTypeOf<Response>()
const { decode } = await unpack()
expectTypeOf(decode.input.transportType).toEqualTypeOf(Transport.http)
expectTypeOf(decode.input.response).toEqualTypeOf<Response>()
const result = await decode()
if (!(result instanceof Error)) {
expectTypeOf(result.response).toEqualTypeOf<Response>()
}
return result
})
})
// test(`anyware hooks are typed to http transport`, () => {
// Graffle.create({ schema }).anyware(async ({ encode }) => {
// expectTypeOf(encode.input.transportType).toEqualTypeOf<TransportHttp>()
// const { pack } = await encode()
// expectTypeOf(pack.input.transportType).toEqualTypeOf(Transport.http)
// const { exchange } = await pack()
// expectTypeOf(exchange.input.transportType).toEqualTypeOf(Transport.http)
// // todo we can statically track the method mode like we do the transport mode
// expectTypeOf(exchange.input.request).toEqualTypeOf<
// requestPipeline.Steps.CoreExchangePostRequest | requestPipeline.Steps.CoreExchangeGetRequest
// >()
// const { unpack } = await exchange()
// expectTypeOf(unpack.input.transportType).toEqualTypeOf(Transport.http)
// expectTypeOf(unpack.input.response).toEqualTypeOf<Response>()
// const { decode } = await unpack()
// expectTypeOf(decode.input.transportType).toEqualTypeOf(Transport.http)
// expectTypeOf(decode.input.response).toEqualTypeOf<Response>()
// const result = await decode()
// if (!(result instanceof Error)) {
// expectTypeOf(result.response).toEqualTypeOf<Response>()
// }
// return result
// })
// })

test(`when envelope is used then response property is present even if relying on schema url default`, async () => {
const service = await serveSchema({ schema: schemaPokemon })
Expand Down
50 changes: 25 additions & 25 deletions src/client/client.transport-memory.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import { expectTypeOf } from 'vitest'
// import { expectTypeOf } from 'vitest'
import { test } from '../../tests/_/helpers.js'
import { schema } from '../../tests/_/schemas/kitchen-sink/schema.js'
import { Graffle } from '../entrypoints/main.js'
import { Transport } from '../types/Transport.js'
// import { Transport } from '../types/Transport.js'

test(`anyware hooks are typed to memory transport`, () => {
Graffle.create({ schema }).anyware(async ({ encode }) => {
expectTypeOf(encode.input.transportType).toEqualTypeOf(Transport.memory)
const { pack } = await encode()
expectTypeOf(pack.input.transportType).toEqualTypeOf(Transport.memory)
const { exchange } = await pack()
expectTypeOf(exchange.input.transportType).toEqualTypeOf(Transport.memory)
const { unpack } = await exchange()
expectTypeOf(unpack.input.transportType).toEqualTypeOf(Transport.memory)
// @ts-expect-error any
unpack.input.response
const { decode } = await unpack()
expectTypeOf(decode.input.transportType).toEqualTypeOf(Transport.memory)
// @ts-expect-error any
decode.input.response
const result = await decode()
if (!(result instanceof Error)) {
// @ts-expect-error any
result.response
}
return result
})
})
// test(`anyware hooks are typed to memory transport`, () => {
// Graffle.create({ schema }).anyware(async ({ encode }) => {
// expectTypeOf(encode.input.transportType).toEqualTypeOf<Transport>()
// const { pack } = await encode()
// expectTypeOf(pack.input.transportType).toEqualTypeOf(Transport.memory)
// const { exchange } = await pack()
// expectTypeOf(exchange.input.transportType).toEqualTypeOf(Transport.memory)
// const { unpack } = await exchange()
// expectTypeOf(unpack.input.transportType).toEqualTypeOf(Transport.memory)
// // @ts-expect-error any
// unpack.input.response
// const { decode } = await unpack()
// expectTypeOf(decode.input.transportType).toEqualTypeOf(Transport.memory)
// // @ts-expect-error any
// decode.input.response
// const result = await decode()
// if (!(result instanceof Error)) {
// // @ts-expect-error any
// result.response
// }
// return result
// })
// })

test(`cannot set headers in constructor`, () => {
// todo: This error is poor for the user. It refers to schema not being a URL. The better message would be that headers is not allowed with memory transport.
Expand Down
43 changes: 17 additions & 26 deletions src/requestPipeline/RequestPipeline.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { FormattedExecutionResult, GraphQLSchema } from 'graphql'
import type { Context } from '../client/context.js'
import type { GraffleExecutionResultEnvelope } from '../client/handleOutput.js'
import type { Config } from '../client/Settings/Config.js'
import { MethodMode, type MethodModeGetReads } from '../client/transportHttp/request.js'
import type { MethodModePost } from '../client/transportHttp/request.js'
import { Anyware } from '../lib/anyware/__.js'
Expand Down Expand Up @@ -207,14 +206,14 @@ export namespace requestPipeline {
// Possible from http transport fetch with abort controller.
// | DOMException

export type Result<$Config extends Config = Config> = Anyware.Pipeline.InferResultFromSpec<Spec<$Config>>
export type Result = Anyware.Pipeline.InferResultFromSpec<Spec>

export type Spec<$Config extends Config = Config> = Anyware.PipelineSpecFromSteps<[
Steps.HookDefEncode<$Config>,
Steps.HookDefPack<$Config>,
Steps.HookDefExchange<$Config>,
Steps.HookDefUnpack<$Config>,
Steps.HookDefDecode<$Config>,
export type Spec = Anyware.PipelineSpecFromSteps<[
Steps.HookDefEncode,
Steps.HookDefPack,
Steps.HookDefExchange,
Steps.HookDefUnpack,
Steps.HookDefDecode,
]>

export namespace Steps {
Expand All @@ -224,40 +223,35 @@ export namespace requestPipeline {

// dprint-ignore

type TransportInput<$Config extends Config, $HttpProperties = {}, $MemoryProperties = {}> =
type TransportInput<$HttpProperties = {}, $MemoryProperties = {}> =
| (
TransportHttp extends $Config['transport']['type']
? ({
({
transportType: TransportHttp
url: string | URL
} & $HttpProperties)
: never
)
| (
TransportMemory extends $Config['transport']['type']
? ({
({
transportType: TransportMemory
schema: GraphQLSchema
} & $MemoryProperties)
: never
)

// ---------------------------

export type HookDefEncode<$Config extends Config = Config> = {
export type HookDefEncode = {
name: `encode`
input:
& { request: Grafaid.RequestAnalyzedInput }
& HookInputBase
& TransportInput<$Config>
& TransportInput
}

export type HookDefPack<$Config extends Config = Config> = {
export type HookDefPack = {
name: `pack`
input:
& HookInputBase
& TransportInput<
$Config,
// todo why is headers here but not other http request properties?
{ headers?: HeadersInit }
>
Expand All @@ -274,41 +268,38 @@ export namespace requestPipeline {
}
}

export type HookDefExchange<$Config extends Config> = {
export type HookDefExchange = {
name: `exchange`
slots: {
fetch: (request: Request) => Response | Promise<Response>
}
input:
& HookInputBase
& TransportInput<
$Config,
{ request: CoreExchangePostRequest | CoreExchangeGetRequest; headers?: HeadersInit },
{ request: Grafaid.HTTP.RequestConfig }
>
}

export type HookDefUnpack<$Config extends Config> = {
export type HookDefUnpack = {
name: `unpack`
input:
& HookInputBase
& TransportInput<
$Config,
{ response: Response },
{ result: FormattedExecutionResult }
>
}

export type HookDefDecode<$Config extends Config> = {
export type HookDefDecode = {
name: `decode`
input:
& HookInputBase
& TransportInput<
$Config,
{ response: Response }
>
& { result: FormattedExecutionResult }
output: GraffleExecutionResultEnvelope<$Config>
output: GraffleExecutionResultEnvelope
}

/**
Expand Down
3 changes: 1 addition & 2 deletions tests/_/SpyExtension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { beforeEach } from 'vitest'
import { createExtension } from '../../src/entrypoints/main.js'
import type { Config } from '../../src/entrypoints/utilities-for-generated.js'
import type { requestPipeline } from '../../src/requestPipeline/__.js'

interface SpyData {
Expand All @@ -11,7 +10,7 @@ interface SpyData {
input: requestPipeline.Steps.HookDefPack['input'] | null
}
exchange: {
input: requestPipeline.Steps.HookDefExchange<Config>['input'] | null
input: requestPipeline.Steps.HookDefExchange['input'] | null
}
}

Expand Down

0 comments on commit aad4cac

Please sign in to comment.