Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graffle): extension system #871

Merged
merged 31 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b67769d
feat(graffle): extension system
jasonkuhrt May 18, 2024
faf48c1
name
jasonkuhrt May 18, 2024
ae5cfd9
rename send to request
jasonkuhrt May 18, 2024
d9fb7b3
extension library
jasonkuhrt May 18, 2024
fe4dad1
maybe async
jasonkuhrt May 19, 2024
af8dd0b
better testing
jasonkuhrt May 19, 2024
9e11992
better function analysis and multi testing
jasonkuhrt May 19, 2024
70dc478
more test cases
jasonkuhrt May 19, 2024
26e4309
implement the core
jasonkuhrt May 20, 2024
d0107e8
typesafe extension
jasonkuhrt May 20, 2024
952b575
integrating into client
jasonkuhrt May 20, 2024
2b52cd4
first working tested integration
jasonkuhrt May 20, 2024
dded27f
work
jasonkuhrt May 20, 2024
c3b1c88
fix document
jasonkuhrt May 20, 2024
c16f91e
add destructure anyware tests
jasonkuhrt May 20, 2024
ea9ac4e
better error handling
jasonkuhrt May 21, 2024
4900880
fixing things
jasonkuhrt May 21, 2024
79c2602
work
jasonkuhrt May 21, 2024
05c4f37
all client tests passing now
jasonkuhrt May 21, 2024
90c722e
Merge branch 'main' into feat/extension-system
jasonkuhrt May 21, 2024
1ce9f87
fix type errors in client
jasonkuhrt May 21, 2024
aaed8c5
fix types
jasonkuhrt May 21, 2024
da2ad59
lint
jasonkuhrt May 21, 2024
558abd3
format
jasonkuhrt May 21, 2024
ae1c5aa
try fix
jasonkuhrt May 21, 2024
f5793fa
try fix
jasonkuhrt May 21, 2024
50daf90
try fix
jasonkuhrt May 21, 2024
ce5315b
refactor
jasonkuhrt May 22, 2024
993a1df
builder api
jasonkuhrt May 23, 2024
8273d16
type tests
jasonkuhrt May 23, 2024
7904014
done
jasonkuhrt May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions DOCUMENTATION_NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,41 @@ You can change the output of client methods by configuring its return mode. This

The only client method that is not affected by return mode is `raw` which will _always_ return a standard GraphQL result type.

Here is a summary table of the modes:

| Mode | Throw Sources (no type safety) | Returns (type safe) |
| ---------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `graphql` | Extensions, Fetch | `GraphQLExecutionResult` |
| `graphqlSuccess` | Extensions, Fetch, GraphQLExecutionResult.errors | `GraphQLExecutionResult` with `.errors` always missing. |
| `data` (default) | Extensions, Fetch, GraphQLExecutionResult.errors | `GraphQLExecutionResult.data` |
| `dataSuccess` | Extensions, Fetch, GraphQLExecutionResult.errors, GraphQLExecutionResult.data Schema Errors | `GraphQLExecutionResult.data` without any schema errors |
| `dataAndErrors` | | `GraphQLExecutionResult.data`, errors from: Extensions, Fetch, GraphQLExecutionResult.errors |

## `graphql`

Return the standard graphql execution output.

## `data`

Return just the data including [schema errors](#schema-errors) (if using). Other errors are thrown as an `AggregateError`.
## `graphqlSuccess`

**This mode is the default.**
Return the standard graphql execution output. However, if there would be any errors then they're thrown as an `AggregateError`.
This mode acts like you were using [`OrThrow`](#orthrow) method variants all the time.

## `successData`
## `dataSuccess`

Return just the data excluding [schema errors](#schema-errors). Errors are thrown as an `AggregateError`. This mode acts like you were using [`OrThrow`](#orthrow) method variants all the time.
Return just the data excluding [schema errors](#schema-errors). Errors are thrown as an `AggregateError`.
This mode acts like you were using [`OrThrow`](#orthrow) method variants all the time.

This mode is only available when using [schema errors](#schema-errors).

## `data`

Return just the data including [schema errors](#schema-errors) (if using). Other errors are thrown as an `AggregateError`.

**This mode is the default.**

## `dataAndErrors`

Return data and errors. This is the most type-safe mode. It never throws.
Return a union type of data and errors. This is the most type-safe mode. It never throws.

# Schema Errors

Expand Down
3 changes: 3 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ export default tsEslint.config({
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
['@typescript-eslint/only-throw-error']: 'off',
},
})
2 changes: 1 addition & 1 deletion src/layers/0_functions/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface Input extends BaseInput {
schema: GraphQLSchema
}

export const execute = async (input: Input): Promise<ExecutionResult<any>> => {
export const execute = async (input: Input): Promise<ExecutionResult> => {
switch (typeof input.document) {
case `string`: {
return await graphql({
Expand Down
6 changes: 4 additions & 2 deletions src/layers/0_functions/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import type { BaseInput } from './types.js'

export type URLInput = URL | string

interface Input extends BaseInput {
export interface NetworkRequestInput extends BaseInput {
url: URLInput
headers?: HeadersInit
}

export type NetworkRequest = (input: NetworkRequestInput) => Promise<ExecutionResult>

/**
* @see https://graphql.github.io/graphql-over-http/draft/
*/
export const request = async (input: Input): Promise<ExecutionResult> => {
export const request: NetworkRequest = async (input) => {
const documentEncoded = typeof input.document === `string` ? input.document : print(input.document)

const body = {
Expand Down
23 changes: 0 additions & 23 deletions src/layers/0_functions/requestOrExecute.ts
Original file line number Diff line number Diff line change
@@ -1,23 +0,0 @@
import type { ExecutionResult, GraphQLSchema } from 'graphql'
import { execute } from './execute.js'
import type { URLInput } from './request.js'
import { request } from './request.js'
import type { BaseInput } from './types.js'

export type SchemaInput = URLInput | GraphQLSchema

export interface Input extends BaseInput {
schema: SchemaInput
}

export const requestOrExecute = async (
input: Input,
): Promise<ExecutionResult> => {
const { schema, ...baseInput } = input

if (schema instanceof URL || typeof schema === `string`) {
return await request({ url: schema, ...baseInput })
}

return await execute({ schema, ...baseInput })
}
23 changes: 18 additions & 5 deletions src/layers/2_generator/globalRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,35 @@ import type { Schema } from '../1_Schema/__.js'

declare global {
export namespace GraphQLRequestTypes {
interface Schemas {
}
interface Schemas {}
// Use this is for manual internal type testing.
// interface SchemasAlwaysEmpty {}
}
}

export type GlobalRegistry = Record<string, {
type SomeSchema = {
index: Schema.Index
customScalars: Record<string, Schema.Scalar.Scalar>
featureOptions: {
schemaErrors: boolean
}
}>
}

type ZeroSchema = {
index: { name: never }
featureOptions: {
schemaErrors: false
}
}

export type GlobalRegistry = Record<string, SomeSchema>

export namespace GlobalRegistry {
export type Schemas = GraphQLRequestTypes.Schemas
export type SchemaList = Values<Schemas>

export type IsEmpty = keyof Schemas extends never ? true : false

export type SchemaList = IsEmpty extends true ? ZeroSchema : Values<Schemas>

export type DefaultSchemaName = 'default'

Expand Down
8 changes: 4 additions & 4 deletions src/layers/3_SelectionSet/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ export interface Context {

export const rootTypeSelectionSet = (
context: Context,
schemaObject: Schema.Object$2,
ss: GraphQLObjectSelection,
objectDef: Schema.Object$2,
selectionSet: GraphQLObjectSelection,
operationName: string = ``,
) => {
const operationTypeName = lowerCaseFirstLetter(schemaObject.fields.__typename.type.type)
return `${operationTypeName} ${operationName} { ${resolveObjectLikeFieldValue(context, schemaObject, ss)} }`
const operationTypeName = lowerCaseFirstLetter(objectDef.fields.__typename.type.type)
return `${operationTypeName} ${operationName} { ${resolveObjectLikeFieldValue(context, objectDef, selectionSet)} }`
}

const resolveDirectives = (fieldValue: FieldValue) => {
Expand Down
10 changes: 9 additions & 1 deletion src/layers/5_client/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,26 @@ import type { SelectionSet } from '../3_SelectionSet/__.js'

export type ReturnModeType =
| ReturnModeTypeGraphQL
| ReturnModeTypeGraphQLSuccess
| ReturnModeTypeSuccessData
| ReturnModeTypeData
| ReturnModeTypeDataAndErrors

export type ReturnModeTypeBase = ReturnModeTypeGraphQL | ReturnModeTypeDataAndErrors | ReturnModeTypeData
export type ReturnModeTypeBase =
| ReturnModeTypeGraphQLSuccess
| ReturnModeTypeGraphQL
| ReturnModeTypeDataAndErrors
| ReturnModeTypeData

export type ReturnModeTypeGraphQLSuccess = 'graphqlSuccess'

export type ReturnModeTypeGraphQL = 'graphql'

export type ReturnModeTypeData = 'data'

export type ReturnModeTypeDataAndErrors = 'dataAndErrors'

// todo rename to dataSuccess
export type ReturnModeTypeSuccessData = 'successData'

export type OptionsInput = {
Expand Down
4 changes: 2 additions & 2 deletions src/layers/5_client/RootTypeMethods.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OperationName } from '../../lib/graphql.js'
import type { OperationTypeName } from '../../lib/graphql.js'
import type { Exact } from '../../lib/prelude.js'
import type { TSError } from '../../lib/TSError.js'
import type { InputFieldsAllNullable, Schema } from '../1_Schema/__.js'
Expand All @@ -23,7 +23,7 @@ type RootTypeFieldContext = {

// dprint-ignore
export type GetRootTypeMethods<$Config extends Config, $Index extends Schema.Index> = {
[$OperationName in OperationName as $Index['Root'][Capitalize<$OperationName>] extends null ? never : $OperationName]:
[$OperationName in OperationTypeName as $Index['Root'][Capitalize<$OperationName>] extends null ? never : $OperationName]:
RootTypeMethods<$Config, $Index, Capitalize<$OperationName>>
}

Expand Down
24 changes: 24 additions & 0 deletions src/layers/5_client/client.batch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, test } from 'vitest'
import { db } from '../../../tests/_/db.js'
import { Graffle } from '../../../tests/_/schema/generated/__.js'
import * as Schema from '../../../tests/_/schema/schema.js'

const graffle = Graffle.create({ schema: Schema.schema })

// dprint-ignore
describe(`query`, () => {
test(`success`, async () => {
await expect(graffle.query.$batch({ id: true })).resolves.toMatchObject({ id:db.id })
})
test(`error`, async () => {
await expect(graffle.query.$batch({ error: true })).rejects.toMatchObject(db.errorAggregate)
})
describe(`orThrow`, () => {
test(`success`, async () => {
await expect(graffle.query.$batchOrThrow({ id: true })).resolves.toMatchObject({ id:db.id })
})
test(`error`, async () => {
await expect(graffle.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.errorAggregate)
})
})
})
4 changes: 2 additions & 2 deletions src/layers/5_client/client.document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe(`document with two queries`, () => {
// @ts-expect-error
await expect(run(`boo`)).rejects.toMatchObject({ errors: [{ message: `Unknown operation named "boo".` }] })
})
test(`error if invalid name in document`, async () => {
test.skip(`error if invalid name in document`, async () => {
// @ts-expect-error
const { run } = graffle.document({ foo$: { query: { id: true } } })
await expect(run(`foo$`)).rejects.toMatchObject({
Expand Down Expand Up @@ -86,7 +86,7 @@ describe(`document(...).runOrThrow()`, () => {
`[Error: Failure on field resultNonNull: ErrorOne]`,
)
})
test(`multiple via alias`, async () => {
test.todo(`multiple via alias`, async () => {
const result = graffle.document({
x: { query: { resultNonNull: { $: { case: `ErrorOne` } }, resultNonNull_as_x: { $: { case: `ErrorOne` } } } },
}).runOrThrow()
Expand Down
40 changes: 40 additions & 0 deletions src/layers/5_client/client.extend.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* eslint-disable */
import { ExecutionResult } from 'graphql'
import { describe, expect } from 'vitest'
import { db } from '../../../tests/_/db.js'
import { createResponse, test } from '../../../tests/_/helpers.js'
import { Graffle } from '../../../tests/_/schema/generated/__.js'
import { GraphQLExecutionResult } from '../../legacy/lib/graphql.js'

const client = Graffle.create({ schema: 'https://foo', returnMode: 'dataAndErrors' })
const headers = { 'x-foo': 'bar' }

// todo each extension added should copy, not mutate the client

describe(`entrypoint request`, () => {
test(`can add header to request`, async ({ fetch }) => {
fetch.mockImplementationOnce(async (input: Request) => {
expect(input.headers.get('x-foo')).toEqual(headers['x-foo'])
return createResponse({ data: { id: db.id } })
})
const client2 = client.extend(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
// expectTypeOf(exchange).toEqualTypeOf<NetworkRequestHook>()
// expect(exchange.input).toEqual({ url: 'https://foo', document: `query { id \n }` })
return await pack({ ...pack.input, headers })
})
expect(await client2.query.id()).toEqual(db.id)
})
test('can chain into exchange', async ({ fetch }) => {
fetch.mockImplementationOnce(async () => {
return createResponse({ data: { id: db.id } })
})
const client2 = client.extend(async ({ pack }) => {
const { exchange } = await pack({ ...pack.input, headers })
return await exchange(exchange.input)
})
expect(await client2.query.id()).toEqual(db.id)
})
})
18 changes: 1 addition & 17 deletions src/layers/5_client/client.rootTypeMethods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,10 @@ describe(`query`, () => {
})
describe(`orThrow`, () => {
test(`without error`, async () => {
await expect(graffle.query.objectWithArgsOrThrow({ $: { id: `x` }, id: true })).resolves.toEqual({ id: `x` })
await expect(graffle.query.objectWithArgsOrThrow({ $: { id: `x` }, id: true })).resolves.toEqual({ id: `x`, __typename: `Object1` })
})
test(`with error`, async () => {
await expect(graffle.query.errorOrThrow()).rejects.toMatchObject(db.errorAggregate)
})
})
describe(`$batch`, () => {
test(`success`, async () => {
await expect(graffle.query.$batch({ id: true })).resolves.toMatchObject({ id:db.id })
})
test(`error`, async () => {
await expect(graffle.query.$batch({ error: true })).rejects.toMatchObject(db.errorAggregate)
})
describe(`orThrow`, () => {
test(`success`, async () => {
await expect(graffle.query.$batchOrThrow({ id: true })).resolves.toMatchObject({ id:db.id })
})
test(`error`, async () => {
await expect(graffle.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.errorAggregate)
})
})
})
})
Loading