Skip to content

Commit

Permalink
fix typename injection on alias and inline fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Oct 1, 2024
1 parent 6d6535d commit ef56db0
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 105 deletions.
11 changes: 1 addition & 10 deletions src/layers/4_generator/generators/MethodsDocument.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// todo remove use of Utils.Aug when schema errors not in use
// todo jsdoc
import { hasMutation, hasQuery } from '../../../lib/graphql-plus/graphql.js'
import { createModuleGenerator } from '../helpers/moduleGenerator.js'
import { ModuleGeneratorSchemaIndex } from './SchemaIndex.js'
import { ModuleGeneratorSelectionSets } from './SelectionSets.js'
Expand All @@ -13,16 +12,8 @@ export const ModuleGeneratorMethodsDocument = createModuleGenerator(
code.push(`import type { Index } from './${ModuleGeneratorSchemaIndex.name}.js'`)
code.push(``)

code.push(
`interface DocumentInput {`,
hasQuery(config.schema.typeMapByKind) ? `query?: Record<string, SelectionSets.Query>` : null,
hasMutation(config.schema.typeMapByKind) ? `mutation?: Record<string, SelectionSets.Mutation>` : null,
`}`,
)
code.push(``)

code.push(`export interface Document<$Config extends Utilities.Config> {
<$Document>(document: Utilities.ExactNonEmpty<$Document, DocumentInput>): Utilities.DocumentRunner<
<$Document>(document: Utilities.ExactNonEmpty<$Document, SelectionSets.$Document>): Utilities.DocumentRunner<
$Config,
Index,
// @ts-expect-error We use Exact instead of constraint on this function. TypeScript does not see that as
Expand Down
17 changes: 15 additions & 2 deletions src/layers/4_generator/generators/SelectionSets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
getNodeKind,
getNodeNameAndKind,
hasCustomScalars,
hasMutation,
hasQuery,
isCustomScalarType,
RootTypeName,
StandardScalarTypeTypeScriptMapping,
Expand Down Expand Up @@ -55,6 +57,19 @@ export const ModuleGeneratorSelectionSets = createModuleGenerator(
}
code.push(``)

code.push(title1(`Document`))
code.push(``)
code.push(
`// Prefix with $ because this is not a schema type. A user could have a schema type named "Document" that this would conflict with.`,
)
code.push(
`export interface $Document {`,
hasQuery(config.schema.typeMapByKind) ? `query?: Record<string, Query>` : null,
hasMutation(config.schema.typeMapByKind) ? `mutation?: Record<string, Mutation>` : null,
`}`,
)
code.push(``)

const typesToRender = [
config.schema.typeMapByKind.GraphQLRootType,
config.schema.typeMapByKind.GraphQLEnumType,
Expand Down Expand Up @@ -86,8 +101,6 @@ export const ModuleGeneratorSelectionSets = createModuleGenerator(
*/
`)
code.push(renderRefDefs({ config, nodes: typesToRender.flat() }))

// console.log(code.join(`\n`))
},
)

Expand Down
61 changes: 1 addition & 60 deletions src/layers/5_core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import { mergeRequestInit, searchParamsAppendAll } from '../../lib/http.js'
import { casesExhausted, getOptionalNullablePropertyOrThrow, throwNull } from '../../lib/prelude.js'
import { execute } from '../0_functions/execute.js'
import type { Schema } from '../1_Schema/__.js'
import { Select } from '../2_Select/__.js'
import { ResultSet } from '../3_ResultSet/__.js'
import { SelectionSetGraphqlMapper } from '../3_SelectionSetGraphqlMapper/__.js'
Expand All @@ -28,65 +27,7 @@ import {
type MethodModeGetReads,
} from '../6_client/transportHttp/request.js'
import { type HookMap, hookNamesOrderedBySequence, type HookSequence } from './hooks.js'

const injectTypenameOnResultFields = (
input: {
operationName: string | undefined
schema: Schema.Index
document: Select.Document.DocumentNormalized
},
): Select.Document.DocumentNormalized => {
const { document, operationName, schema } = input
const operation = operationName ? document.operations[operationName] : Object.values(document.operations)[0]!
if (!operation) {
throw new Error(`Operation not found`)
}

injectTypenameOnResultFields_({
operation,
schema,
})

return document
}

const injectTypenameOnResultFields_ = (
input: {
schema: Schema.Index
operation: Select.Document.OperationNormalized
},
): void => {
const { operation, schema } = input

for (const [rootFieldName, fieldValue] of Object.entries(operation.selectionSet)) {
const isResultField = Boolean(schema.error.rootResultFields[operation.rootType][rootFieldName])
if (!isResultField) continue

const field = Select.parseSelection(rootFieldName, fieldValue)

// todo test case for following todo
// todo: handle inline fragments on the root type

switch (field.type) {
case `SelectionSet`: {
field.selectionSet[`__typename`] = true
continue
}
case `Alias`: {
field.aliases.map(alias => {
// todo test case for following todo
// todo: handle inline fragments within the alias selection set
const selectionSet = alias[1] as Select.SelectionSet.AnySelectionSet
selectionSet[`__typename`] = true
})
break
}
default: {
throw new Error(`Unsupported selection set select type for a root field: ${field.type}`)
}
}
}
}
import { injectTypenameOnResultFields } from './schemaErrors.js'

export const anyware = Anyware.create<HookSequence, HookMap, ExecutionResult>({
// If core errors caused by an abort error then raise it as a direct error.
Expand Down
29 changes: 29 additions & 0 deletions src/layers/5_core/schemaErrors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from 'vitest'
import { $Index as schema } from '../../../tests/_/schemas/kitchen-sink/graffle/modules/SchemaRuntime.js'
import type { Query } from '../../../tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.js'
import { Select } from '../2_Select/__.js'
import { injectTypenameOnResultFields } from './schemaErrors.js'

type CasesQuery = [description: string, queryWithoutTypename: Query, queryWithTypename: Query]

// todo symmetrical type tests for these cases
// dprint-ignore
test.each<CasesQuery>([
[`one result field`, { resultNonNull: { } }, { resultNonNull: { __typename: true } }],
[`two result fields`, { resultNonNull: { }, result: { $: { case: `ErrorOne` }}}, { resultNonNull: { __typename: true }, result: { $: { case: `ErrorOne` }, __typename: true } }],
[`no result fields`, { id: true, object: { id: true } }, { id: true, object: { id: true }}],
[`__typename in fragment`, { resultNonNull: { ___: { __typename: true }}}, { resultNonNull: { ___: { __typename: true }, __typename: true } }],
[`root field in fragment`, { ___: { resultNonNull: {} } }, { ___: { resultNonNull: { __typename: true }}}],
[`root field in fragment in alias`, { ___: { resultNonNull: [`x`, {}] } }, { ___: { resultNonNull: [`x`, { __typename: true }] }}],
[`root field alias `, { resultNonNull: [`x`, {}] }, { resultNonNull: [`x`, { __typename: true }] }],
])(`Query %s`, (_, queryWithoutTypenameInput, queryWithTypenameInput) => {
const documentWithTypename = Select.Document.normalizeOrThrow({ query: { x: queryWithTypenameInput as any } })
const documentWithoutTypename = Select.Document.normalizeOrThrow({ query: { x: queryWithoutTypenameInput as any } })

injectTypenameOnResultFields({
document:documentWithoutTypename,
schema,
})

expect(documentWithoutTypename).toMatchObject(documentWithTypename)
})
74 changes: 74 additions & 0 deletions src/layers/5_core/schemaErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { RootTypeName } from '../../lib/graphql-plus/graphql.js'
import type { Schema } from '../1_Schema/__.js'
import { Select } from '../2_Select/__.js'

export const injectTypenameOnResultFields = (
input: {
operationName?: string | undefined
schema: Schema.Index
document: Select.Document.DocumentNormalized
},
): Select.Document.DocumentNormalized => {
const { document, operationName, schema } = input
const operation = operationName ? document.operations[operationName] : Object.values(document.operations)[0]!

if (!operation) {
throw new Error(`Operation not found`)
}

injectTypenameOnRootResultFields({
rootTypeName: operation.rootType,
schema,
selectionSet: operation.selectionSet,
})

return document
}

const injectTypenameOnRootResultFields = (
input: {
schema: Schema.Index
selectionSet: Select.SelectionSet.AnySelectionSet
rootTypeName: RootTypeName
},
): void => {
const { selectionSet, schema, rootTypeName } = input

for (const [rootFieldName, fieldValue] of Object.entries(selectionSet)) {
const field = Select.parseSelection(rootFieldName, fieldValue)

switch (field.type) {
case `InlineFragment`: {
// we need to check contents for result root fields
for (const inlineFragmentSelectionSet of field.selectionSets) {
injectTypenameOnRootResultFields({
rootTypeName,
schema,
selectionSet: inlineFragmentSelectionSet,
})
}
continue
}
case `SelectionSet`: {
if (schema.error.rootResultFields[rootTypeName][rootFieldName]) {
field.selectionSet[`__typename`] = true
}
continue
}
case `Alias`: {
if (schema.error.rootResultFields[rootTypeName][rootFieldName]) {
for (const alias of field.aliases) {
// Casting type: This alias is for a field whose type is in rootResultFields
// so it must be a selection set (e.g. not an indicator)
const aliasSelectionSet = alias[1] as Select.SelectionSet.AnySelectionSet
aliasSelectionSet[`__typename`] = true
}
}
continue
}
default: {
continue
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import type * as Utilities from '../../../../../../src/entrypoints/utilities-for
import type { Index } from './SchemaIndex.js'
import type * as SelectionSets from './SelectionSets.js'

interface DocumentInput {
query?: Record<string, SelectionSets.Query>
mutation?: Record<string, SelectionSets.Mutation>
}

export interface Document<$Config extends Utilities.Config> {
<$Document>(document: Utilities.ExactNonEmpty<$Document, DocumentInput>): Utilities.DocumentRunner<
<$Document>(document: Utilities.ExactNonEmpty<$Document, SelectionSets.$Document>): Utilities.DocumentRunner<
$Config,
Index,
// @ts-expect-error We use Exact instead of constraint on this function. TypeScript does not see that as
Expand Down
22 changes: 22 additions & 0 deletions tests/_/schemas/kitchen-sink/graffle/modules/SelectionSets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@ import type { Select as $Select } from '../../../../../../src/entrypoints/schema
import type * as $Utilities from '../../../../../../src/entrypoints/utilities-for-generated.js'
import type * as $Scalar from './Scalar.js'

//
//
//
//
//
//
// ==================================================================================================
// Document
// ==================================================================================================
//
//
//
//
//
//

// Prefix with $ because this is not a schema type. A user could have a schema type named "Document" that this would conflict with.
export interface $Document {
query?: Record<string, Query>
mutation?: Record<string, Mutation>
}

//
//
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import type * as Utilities from '../../../../../../src/entrypoints/utilities-for
import type { Index } from './SchemaIndex.js'
import type * as SelectionSets from './SelectionSets.js'

interface DocumentInput {
mutation?: Record<string, SelectionSets.Mutation>
}

export interface Document<$Config extends Utilities.Config> {
<$Document>(document: Utilities.ExactNonEmpty<$Document, DocumentInput>): Utilities.DocumentRunner<
<$Document>(document: Utilities.ExactNonEmpty<$Document, SelectionSets.$Document>): Utilities.DocumentRunner<
$Config,
Index,
// @ts-expect-error We use Exact instead of constraint on this function. TypeScript does not see that as
Expand Down
21 changes: 21 additions & 0 deletions tests/_/schemas/mutation-only/graffle/modules/SelectionSets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import type { Select as $Select } from '../../../../../../src/entrypoints/schema.js'
import type * as $Utilities from '../../../../../../src/entrypoints/utilities-for-generated.js'

//
//
//
//
//
//
// ==================================================================================================
// Document
// ==================================================================================================
//
//
//
//
//
//

// Prefix with $ because this is not a schema type. A user could have a schema type named "Document" that this would conflict with.
export interface $Document {
mutation?: Record<string, Mutation>
}

//
//
//
Expand Down
7 changes: 1 addition & 6 deletions tests/_/schemas/pokemon/graffle/modules/MethodsDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import type * as Utilities from '../../../../../../src/entrypoints/utilities-for
import type { Index } from './SchemaIndex.js'
import type * as SelectionSets from './SelectionSets.js'

interface DocumentInput {
query?: Record<string, SelectionSets.Query>
mutation?: Record<string, SelectionSets.Mutation>
}

export interface Document<$Config extends Utilities.Config> {
<$Document>(document: Utilities.ExactNonEmpty<$Document, DocumentInput>): Utilities.DocumentRunner<
<$Document>(document: Utilities.ExactNonEmpty<$Document, SelectionSets.$Document>): Utilities.DocumentRunner<
$Config,
Index,
// @ts-expect-error We use Exact instead of constraint on this function. TypeScript does not see that as
Expand Down
22 changes: 22 additions & 0 deletions tests/_/schemas/pokemon/graffle/modules/SelectionSets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
import type { Select as $Select } from '../../../../../../src/entrypoints/schema.js'
import type * as $Utilities from '../../../../../../src/entrypoints/utilities-for-generated.js'

//
//
//
//
//
//
// ==================================================================================================
// Document
// ==================================================================================================
//
//
//
//
//
//

// Prefix with $ because this is not a schema type. A user could have a schema type named "Document" that this would conflict with.
export interface $Document {
query?: Record<string, Query>
mutation?: Record<string, Mutation>
}

//
//
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import type * as Utilities from '../../../../../../src/entrypoints/utilities-for
import type { Index } from './SchemaIndex.js'
import type * as SelectionSets from './SelectionSets.js'

interface DocumentInput {
query?: Record<string, SelectionSets.Query>
}

export interface Document<$Config extends Utilities.Config> {
<$Document>(document: Utilities.ExactNonEmpty<$Document, DocumentInput>): Utilities.DocumentRunner<
<$Document>(document: Utilities.ExactNonEmpty<$Document, SelectionSets.$Document>): Utilities.DocumentRunner<
$Config,
Index,
// @ts-expect-error We use Exact instead of constraint on this function. TypeScript does not see that as
Expand Down
Loading

0 comments on commit ef56db0

Please sign in to comment.