Skip to content

Commit

Permalink
fix(ts-client): alias encoding (#809)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Apr 29, 2024
1 parent 4ac0cd1 commit 54da7bd
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 87 deletions.
4 changes: 2 additions & 2 deletions src/layers/2_generator/__snapshots__/files.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export namespace Root {
resultNonNull: $.Field<
Union.Result,
$.Args<{
case: Enum.Case
case: $.Input.Nullable<Enum.Case>
}>
>
string: $.Field<$.Output.Nullable<$Scalar.String>, null>
Expand Down Expand Up @@ -705,7 +705,7 @@ export const Query = $.Object$(\`Query\`, {
}),
),
result: $.field($.Output.Nullable(() => Result), $.Args({ case: Case })),
resultNonNull: $.field(() => Result, $.Args({ case: Case })),
resultNonNull: $.field(() => Result, $.Args({ case: $.Input.Nullable(Case) })),
string: $.field($.Output.Nullable($Scalar.String)),
stringWithArgEnum: $.field($.Output.Nullable($Scalar.String), $.Args({ ABCEnum: $.Input.Nullable(ABCEnum) })),
stringWithArgInputObject: $.field(
Expand Down
10 changes: 5 additions & 5 deletions src/layers/3_SelectionSet/__snapshots__/encode.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ exports[`alias > Query 1`] = `
}
--------------
{
id: x
x: id
}
"
`;
Expand All @@ -565,8 +565,8 @@ exports[`alias > Query 2`] = `
}
--------------
{
id: x
id: id2
x: id
id2: id
}
"
`;
Expand All @@ -580,7 +580,7 @@ exports[`alias > Query 3`] = `
}
--------------
{
id: x @skip(if: true)
x: id @skip(if: true)
}
"
`;
Expand All @@ -595,7 +595,7 @@ exports[`alias > Query 4`] = `
}
--------------
{
object: x @skip(if: true) {
x: object @skip(if: true) {
id
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/layers/3_SelectionSet/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ export const resolveObjectLikeFieldValue = (
const fieldName = parseClientFieldName(clientFieldName)
const schemaField = schemaItem.fields[fieldName.actual]
if (!schemaField) throw new Error(`Field ${clientFieldName} not found in schema object`)
/**
* Inject __typename field for result fields that are missing it.
*/
// dprint-ignore
if (rootTypeName && context.config.returnMode === `successData` && context.schemaIndex.error.rootResultFields[rootTypeName][fieldName.actual]) {
(ss as Record<string, boolean>)[`__typename`] = true
Expand Down
3 changes: 2 additions & 1 deletion src/layers/3_SelectionSet/runtime/FieldName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export const parseClientFieldName = (field: string): FieldName => {

export const toGraphQLFieldName = (fieldName: FieldName) => {
if (fieldName.alias) {
return `${fieldName.actual}: ${fieldName.alias}`
// todo test coverage for this, discovered broken, not tested
return `${fieldName.alias}: ${fieldName.actual}`
} else {
return fieldName.actual
}
Expand Down
3 changes: 3 additions & 0 deletions src/layers/3_SelectionSet/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ export type ParseAliasExpression<E> =

export type AliasNameOrigin<N> = ParseAliasExpression<N> extends Alias<infer O, any> ? O : N

/**
* Resolve the target of an alias or if is not an alias just pass through the name.
*/
export type AliasNameTarget<N> = ParseAliasExpression<N> extends Alias<any, infer T> ? T : N

export type ResolveAliasTargets<SelectionSet> = {
Expand Down
15 changes: 12 additions & 3 deletions src/layers/5_client/Config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ExecutionResult } from 'graphql'
import type { GraphQLExecutionResultError } from '../../lib/graphql.js'
import type { SetProperty } from '../../lib/prelude.js'
import type { SetProperty, StringKeyof } from '../../lib/prelude.js'
import type { Schema } from '../1_Schema/__.js'
import type { GlobalRegistry } from '../2_generator/globalRegistry.js'
import type { SelectionSet } from '../3_SelectionSet/__.js'

export type ReturnModeType =
| ReturnModeTypeGraphQL
Expand Down Expand Up @@ -76,14 +77,22 @@ export type IsNeedSelectionTypename<$Config extends Config, $Index extends Schem
$Config['returnMode'] extends 'successData' ? GlobalRegistry.HasSchemaErrors<$Index['name']> extends true ? true :
false :
false

export type AugmentRootTypeSelectionWithTypename<
$Config extends Config,
$Index extends Schema.Index,
$RootTypeName extends Schema.RootTypeName,
$Selection extends object,
> = IsNeedSelectionTypename<$Config, $Index> extends true ? {
[$Key in keyof $Selection]:
[$Key in StringKeyof<$Selection>]:
& $Selection[$Key]
& ($Key extends keyof $Index['error']['rootResultFields'][$RootTypeName] ? TypenameSelection : {}) // eslint-disable-line
& (IsRootFieldNameAResultField<$Index, $RootTypeName, $Key> extends true ? TypenameSelection : {}) // eslint-disable-line
}
: $Selection

type IsRootFieldNameAResultField<
$Index extends Schema.Index,
$RootTypeName extends Schema.RootTypeName,
$FieldName extends string,
> = SelectionSet.AliasNameOrigin<$FieldName> extends keyof $Index['error']['rootResultFields'][$RootTypeName] ? true
: false
41 changes: 30 additions & 11 deletions src/layers/5_client/client.document.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,26 +51,45 @@ describe(`input`, () => {
})
})

describe(`output`, () => {
test(`document with one query`, async () => {
describe(`document(...).run()`, () => {
test(`document with one query`, () => {
{
const result = await client.document({ foo: { query: { id: true } } }).run()
expectTypeOf(result).toEqualTypeOf<{ id: string | null }>()
const result = client.document({ x: { query: { id: true } } }).run()
expectTypeOf(result).resolves.toEqualTypeOf<{ id: string | null }>()
}
{
const result = await client.document({ foo: { query: { id: true } } }).run(`foo`)
expectTypeOf(result).toEqualTypeOf<{ id: string | null }>()
const result = client.document({ x: { query: { id: true } } }).run(`x`)
expectTypeOf(result).resolves.toEqualTypeOf<{ id: string | null }>()
}
{
const result = await client.document({ foo: { query: { id: true } } }).run(undefined)
expectTypeOf(result).toEqualTypeOf<{ id: string | null }>()
const result = client.document({ x: { query: { id: true } } }).run(undefined)
expectTypeOf(result).resolves.toEqualTypeOf<{ id: string | null }>()
}
})
test(`document with two queries`, async () => {
const result = await client.document({
test(`document with two queries`, () => {
const result = client.document({
foo: { query: { id: true } },
bar: { query: { id: true } },
}).run(`foo`)
expectTypeOf(result).toEqualTypeOf<{ id: string | null }>()
expectTypeOf(result).resolves.toEqualTypeOf<{ id: string | null }>()
})
})

describe(`document(...).runOrThrow()`, () => {
describe(`query result field`, () => {
test(`with __typename`, () => {
const result = client.document({ x: { query: { resultNonNull: { __typename: true } } } }).runOrThrow()
expectTypeOf(result).resolves.toEqualTypeOf<{ resultNonNull: { __typename: 'Object1' } }>()
})
test(`without __typename`, () => {
const result = client.document({ x: { query: { resultNonNull: {} } } }).runOrThrow()
expectTypeOf(result).resolves.toEqualTypeOf<{ resultNonNull: { __typename: 'Object1' } }>()
})
test(`multiple via alias`, () => {
const result = client.document({ x: { query: { resultNonNull: {}, resultNonNull_as_x: {} } } }).runOrThrow()
expectTypeOf(result).resolves.toEqualTypeOf<
{ resultNonNull: { __typename: 'Object1' }; x: { __typename: 'Object1' } }
>()
})
})
})
24 changes: 24 additions & 0 deletions src/layers/5_client/client.document.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,27 @@ test(`document with one mutation and one query`, async () => {
await expect(run(`foo`)).resolves.toEqual({ id: db.id1 })
await expect(run(`bar`)).resolves.toEqual({ idNonNull: db.id1 })
})

describe(`document(...).runOrThrow()`, () => {
describe(`query result field`, () => {
test(`with __typename`, async () => {
const result = client.document({ x: { query: { resultNonNull: { $: { case: `ErrorOne` }, __typename: true } } } })
.runOrThrow()
await expect(result).rejects.toMatchInlineSnapshot(`[Error: Failure on field resultNonNull: ErrorOne]`)
})
test(`without __typename`, async () => {
const result = client.document({ x: { query: { resultNonNull: { $: { case: `ErrorOne` } } } } }).runOrThrow()
await expect(result).rejects.toMatchInlineSnapshot(
`[Error: Failure on field resultNonNull: ErrorOne]`,
)
})
test(`multiple via alias`, async () => {
const result = client.document({
x: { query: { resultNonNull: { $: { case: `ErrorOne` } }, resultNonNull_as_x: { $: { case: `ErrorOne` } } } },
}).runOrThrow()
await expect(result).rejects.toMatchInlineSnapshot(
`[ContextualAggregateError: Two or more schema errors in the execution result.]`,
)
})
})
})
129 changes: 70 additions & 59 deletions src/layers/5_client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export const create: Create = (
},
} as GraphQLObjectSelection
const result = await rootObjectExecutors[rootTypeName](documentObject)
const resultHandled = handleReturn(result)
const resultHandled = handleReturn(input.schemaIndex, result, returnMode)
if (resultHandled instanceof Error) return resultHandled
return returnMode === `data` || returnMode === `dataAndErrors` || returnMode === `successData`
// @ts-expect-error make this type safe?
Expand All @@ -195,54 +195,6 @@ export const create: Create = (
}
}

const handleReturn = (result: ExecutionResult) => {
switch (returnMode) {
case `dataAndErrors`:
case `successData`:
case `data`: {
if (result.errors && result.errors.length > 0) {
const error = new Errors.ContextualAggregateError(
`One or more errors in the execution result.`,
{},
result.errors,
)
if (returnMode === `data` || returnMode === `successData`) throw error
return error
}
if (returnMode === `successData`) {
if (!isPlainObject(result.data)) throw new Error(`Expected data to be an object.`)
const schemaErrors = Object.entries(result.data).map(([rootFieldName, rootFieldValue]) => {
// todo do not hardcode root type
const isResultField = Boolean(input.schemaIndex.error.rootResultFields.Query[rootFieldName])
if (!isResultField) return null
if (!isPlainObject(rootFieldValue)) return new Error(`Expected result field to be an object.`)
const __typename = rootFieldValue[`__typename`]
if (typeof __typename !== `string`) throw new Error(`Expected __typename to be selected and a string.`)
const isErrorObject = Boolean(
input.schemaIndex.error.objectsTypename[__typename],
)
if (!isErrorObject) return null
// todo extract message
return new Error(`Failure on field ${rootFieldName}: ${__typename}`)
}).filter((_): _ is Error => _ !== null)
if (schemaErrors.length === 1) throw schemaErrors[0]!
if (schemaErrors.length > 0) {
const error = new Errors.ContextualAggregateError(
`One or more schema errors in the execution result.`,
{},
schemaErrors,
)
throw error
}
}
return result.data
}
default: {
return result
}
}
}

const rootObjectExecutors = {
Mutation: executeRootType(`Mutation`),
Query: executeRootType(`Query`),
Expand All @@ -262,7 +214,7 @@ export const create: Create = (
const resultRaw = await rootObjectExecutors[rootTypeName]({
[rootTypeNameToOperationName[rootTypeName]]: selectionSetOrIndicator,
})
const result = handleReturn(resultRaw)
const result = handleReturn(input.schemaIndex, resultRaw, returnMode)
if (isOrThrow && result instanceof Error) throw result
// todo consolidate
// @ts-expect-error fixme
Expand Down Expand Up @@ -338,19 +290,26 @@ export const create: Create = (
operationName,
// todo variables
})
return handleReturn(result)
return handleReturn(input.schemaIndex, result, returnMode)
}
return {
run,
runOrThrow: async (operationName: string) => {
const result = await run(operationName)
if (result instanceof Error) throw result
// @ts-expect-error fixme
if (returnMode === `graphql` && result.errors && result.errors.length > 0) {
// @ts-expect-error fixme
throw new Errors.ContextualAggregateError(`One or more errors in the execution result.`, {}, result.errors)
}
return result
const documentString = toDocumentString({
...encodeContext,
config: {
...encodeContext.config,
returnMode: `successData`,
},
}, documentObject)
const result = await executeGraphQLDocument({
document: documentString,
operationName,
// todo variables
})
// todo refactor...
const resultReturn = handleReturn(input.schemaIndex, result, `successData`)
return returnMode === `graphql` ? result : resultReturn
},
}
},
Expand All @@ -362,3 +321,55 @@ export const create: Create = (

return client
}

const handleReturn = (schemaIndex: Schema.Index, result: ExecutionResult, returnMode: ReturnModeType) => {
switch (returnMode) {
case `dataAndErrors`:
case `successData`:
case `data`: {
if (result.errors && result.errors.length > 0) {
const error = new Errors.ContextualAggregateError(
`One or more errors in the execution result.`,
{},
result.errors,
)
if (returnMode === `data` || returnMode === `successData`) throw error
return error
}
if (returnMode === `successData`) {
if (!isPlainObject(result.data)) throw new Error(`Expected data to be an object.`)
const schemaErrors = Object.entries(result.data).map(([rootFieldName, rootFieldValue]) => {
// todo this check would be nice but it doesn't account for aliases right now. To achieve this we would
// need to have the selection set available to use and then do a costly analysis for all fields that were aliases.
// So costly that we would probably instead want to create an index of them on the initial encoding step and
// then make available down stream. Also, note, here, the hardcoding of Query, needs to be any root type.
// const isResultField = Boolean(schemaIndex.error.rootResultFields.Query[rootFieldName])
// if (!isResultField) return null
// if (!isPlainObject(rootFieldValue)) return new Error(`Expected result field to be an object.`)
if (!isPlainObject(rootFieldValue)) return null
const __typename = rootFieldValue[`__typename`]
if (typeof __typename !== `string`) throw new Error(`Expected __typename to be selected and a string.`)
const isErrorObject = Boolean(
schemaIndex.error.objectsTypename[__typename],
)
if (!isErrorObject) return null
// todo extract message
return new Error(`Failure on field ${rootFieldName}: ${__typename}`)
}).filter((_): _ is Error => _ !== null)
if (schemaErrors.length === 1) throw schemaErrors[0]!
if (schemaErrors.length > 0) {
const error = new Errors.ContextualAggregateError(
`Two or more schema errors in the execution result.`,
{},
schemaErrors,
)
throw error
}
}
return result.data
}
default: {
return result
}
}
}
2 changes: 2 additions & 0 deletions src/lib/prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,3 +245,5 @@ export function assertArray(v: unknown): asserts v is unknown[] {
export function assertObject(v: unknown): asserts v is object {
if (v === null || typeof v !== `object`) throw new Error(`Expected object. Got: ${String(v)}`)
}

export type StringKeyof<T> = keyof T & string
2 changes: 1 addition & 1 deletion tests/_/schema/generated/SchemaBuildtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export namespace Root {
resultNonNull: $.Field<
Union.Result,
$.Args<{
case: Enum.Case
case: $.Input.Nullable<Enum.Case>
}>
>
string: $.Field<$.Output.Nullable<$Scalar.String>, null>
Expand Down
2 changes: 1 addition & 1 deletion tests/_/schema/generated/SchemaRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export const Query = $.Object$(`Query`, {
}),
),
result: $.field($.Output.Nullable(() => Result), $.Args({ case: Case })),
resultNonNull: $.field(() => Result, $.Args({ case: Case })),
resultNonNull: $.field(() => Result, $.Args({ case: $.Input.Nullable(Case) })),
string: $.field($.Output.Nullable($Scalar.String)),
stringWithArgEnum: $.field($.Output.Nullable($Scalar.String), $.Args({ ABCEnum: $.Input.Nullable(ABCEnum) })),
stringWithArgInputObject: $.field(
Expand Down

0 comments on commit 54da7bd

Please sign in to comment.