Skip to content

Commit 7f03d24

Browse files
authored
Fix runtime error for complex synth generics e.g. kysely wrappers (#366)
* Add test setup * Remove logic to get TS Type (originally to use with checker.isAssignableTo) * Update e2e tests * Add changeset * Update unit tests
1 parent 5f1a62d commit 7f03d24

File tree

13 files changed

+431
-25
lines changed

13 files changed

+431
-25
lines changed

.changeset/polite-numbers-love.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@eddeee888/gcg-typescript-resolver-files': patch
3+
---
4+
5+
Avoid low-level TypeScript typechecker usage unncessarily, which breaks when running static analysis
6+
7+
Originally, `getType` function was called during static analysis to get low-level `Type` for each type property declarations. These types can be used in typechecker's `isAssignableTo` function, which would have a significant perf boost (if done right). However, there needs to be a significant change to use `.isAssignableTo`, so `getType` was left there so we can continue the work later.
8+
9+
However, declarations of a node could be `undefined` if the type is wrapped in generics that synthesies properties(?). This causes the runtime error. [Relevant convo](https://github.com/microsoft/TypeScript/issues/61697)
10+
11+
For now, we can just remove the `getType` call, until we know how to handle these typing issues.

packages/typescript-resolver-files-e2e/project.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@
158158
"rimraf -g \"packages/typescript-resolver-files-e2e/src/test-deep-modules/**/generated\""
159159
],
160160
"parallel": false
161+
},
162+
"test-complex-synth-generic-wrapper": {
163+
"commands": [
164+
"rimraf -g \"packages/typescript-resolver-files-e2e/src/test-complex-synth-generic-wrapper/**/resolvers/\"",
165+
"rimraf -g \"packages/typescript-resolver-files-e2e/src/test-complex-synth-generic-wrapper/**/*.generated.*\""
166+
],
167+
"parallel": false
161168
}
162169
}
163170
},
@@ -183,7 +190,8 @@
183190
"nx graphql-codegen typescript-resolver-files-e2e -c test-nested-domain-modules --verbose",
184191
"nx graphql-codegen typescript-resolver-files-e2e -c test-resolvers-auto-wireup --verbose",
185192
"nx graphql-codegen typescript-resolver-files-e2e -c test-federation --verbose",
186-
"nx graphql-codegen typescript-resolver-files-e2e -c test-deep-modules --verbose"
193+
"nx graphql-codegen typescript-resolver-files-e2e -c test-deep-modules --verbose",
194+
"nx graphql-codegen typescript-resolver-files-e2e -c test-complex-synth-generic-wrapper --verbose"
187195
],
188196
"parallel": false
189197
},
@@ -258,6 +266,9 @@
258266
},
259267
"test-deep-modules": {
260268
"configFile": "packages/typescript-resolver-files-e2e/src/test-deep-modules/codegen.ts"
269+
},
270+
"test-complex-synth-generic-wrapper": {
271+
"configFile": "packages/typescript-resolver-files-e2e/src/test-complex-synth-generic-wrapper/codegen.ts"
261272
}
262273
},
263274
"dependsOn": ["prepare-e2e-modules"]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { CodegenConfig } from '@graphql-codegen/cli';
2+
import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files';
3+
4+
const config: CodegenConfig = {
5+
hooks: {
6+
afterAllFileWrite: ['prettier --write'],
7+
},
8+
generates: {
9+
'packages/typescript-resolver-files-e2e/src/test-complex-synth-generic-wrapper/schema':
10+
defineConfig(
11+
{ resolverGeneration: 'minimal' },
12+
{
13+
schema: [
14+
'packages/typescript-resolver-files-e2e/src/test-complex-synth-generic-wrapper/**/*.graphqls',
15+
'packages/typescript-resolver-files-e2e/src/test-complex-synth-generic-wrapper/**/*.graphqls.ts',
16+
],
17+
}
18+
),
19+
},
20+
};
21+
22+
export default config;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Query {
2+
book(id: ID!): Book
3+
}
4+
5+
type Book {
6+
id: ID!
7+
isbn: String!
8+
nextBookInSeries: Book
9+
previousBookInSeries: Book
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Start: types from kysely
2+
type Generated<S> = ColumnType<S, S | undefined, S>;
3+
type ColumnType<
4+
SelectType,
5+
InsertType = SelectType,
6+
UpdateType = SelectType
7+
> = {
8+
readonly __select__: SelectType;
9+
readonly __insert__: InsertType;
10+
readonly __update__: UpdateType;
11+
};
12+
type DrainOuterGeneric<T> = [T] extends [unknown] ? T : never;
13+
type IfNotNever<T, K> = T extends never ? never : K;
14+
type SelectType<T> = T extends ColumnType<infer S, any, any> ? S : T;
15+
type NonNeverSelectKeys<R> = {
16+
[K in keyof R]: IfNotNever<SelectType<R[K]>, K>;
17+
}[keyof R];
18+
type Selectable<R> = DrainOuterGeneric<{
19+
[K in NonNeverSelectKeys<R>]: SelectType<R[K]>;
20+
}>;
21+
// End: types from kysely
22+
23+
interface BookTable {
24+
id: Generated<number>;
25+
isbn: string;
26+
next_book_in_series_id: number | null;
27+
previous_book_in_series_id: number | null;
28+
}
29+
30+
export type BookMapper = Selectable<BookTable>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { BookResolvers } from './../../types.generated';
2+
/*
3+
* Note: This object type is generated because "BookMapper" is declared. This is to ensure runtime safety.
4+
*
5+
* When a mapper is used, it is possible to hit runtime errors in some scenarios:
6+
* - given a field name, the schema type's field type does not match mapper's field type
7+
* - or a schema type's field does not exist in the mapper's fields
8+
*
9+
* If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config.
10+
*/
11+
export const Book: BookResolvers = {
12+
/* Implement Book resolver logic here */
13+
nextBookInSeries: async (_parent, _arg, _ctx) => {
14+
/* Book.nextBookInSeries resolver is required because Book.nextBookInSeries exists but BookMapper.nextBookInSeries does not */
15+
},
16+
previousBookInSeries: async (_parent, _arg, _ctx) => {
17+
/* Book.previousBookInSeries resolver is required because Book.previousBookInSeries exists but BookMapper.previousBookInSeries does not */
18+
},
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { QueryResolvers } from './../../../types.generated';
2+
export const book: NonNullable<QueryResolvers['book']> = async (
3+
_parent,
4+
_arg,
5+
_ctx
6+
) => {
7+
/* Implement Query.book resolver logic here */
8+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* This file was automatically generated. DO NOT UPDATE MANUALLY. */
2+
import type { Resolvers } from './types.generated';
3+
import { book as Query_book } from './base/resolvers/Query/book';
4+
import { Book } from './base/resolvers/Book';
5+
export const resolvers: Resolvers = {
6+
Query: { book: Query_book },
7+
8+
Book: Book,
9+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Book {
2+
id: ID!
3+
isbn: String!
4+
nextBookInSeries: Book
5+
previousBookInSeries: Book
6+
}
7+
8+
type Query {
9+
book(id: ID!): Book
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import type { DocumentNode } from 'graphql';
2+
export const typeDefs = {
3+
kind: 'Document',
4+
definitions: [
5+
{
6+
kind: 'ObjectTypeDefinition',
7+
name: { kind: 'Name', value: 'Query' },
8+
interfaces: [],
9+
directives: [],
10+
fields: [
11+
{
12+
kind: 'FieldDefinition',
13+
name: { kind: 'Name', value: 'book' },
14+
arguments: [
15+
{
16+
kind: 'InputValueDefinition',
17+
name: { kind: 'Name', value: 'id' },
18+
type: {
19+
kind: 'NonNullType',
20+
type: {
21+
kind: 'NamedType',
22+
name: { kind: 'Name', value: 'ID' },
23+
},
24+
},
25+
directives: [],
26+
},
27+
],
28+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Book' } },
29+
directives: [],
30+
},
31+
],
32+
},
33+
{
34+
kind: 'ObjectTypeDefinition',
35+
name: { kind: 'Name', value: 'Book' },
36+
interfaces: [],
37+
directives: [],
38+
fields: [
39+
{
40+
kind: 'FieldDefinition',
41+
name: { kind: 'Name', value: 'id' },
42+
arguments: [],
43+
type: {
44+
kind: 'NonNullType',
45+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'ID' } },
46+
},
47+
directives: [],
48+
},
49+
{
50+
kind: 'FieldDefinition',
51+
name: { kind: 'Name', value: 'isbn' },
52+
arguments: [],
53+
type: {
54+
kind: 'NonNullType',
55+
type: {
56+
kind: 'NamedType',
57+
name: { kind: 'Name', value: 'String' },
58+
},
59+
},
60+
directives: [],
61+
},
62+
{
63+
kind: 'FieldDefinition',
64+
name: { kind: 'Name', value: 'nextBookInSeries' },
65+
arguments: [],
66+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Book' } },
67+
directives: [],
68+
},
69+
{
70+
kind: 'FieldDefinition',
71+
name: { kind: 'Name', value: 'previousBookInSeries' },
72+
arguments: [],
73+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Book' } },
74+
directives: [],
75+
},
76+
],
77+
},
78+
{
79+
kind: 'SchemaDefinition',
80+
operationTypes: [
81+
{
82+
kind: 'OperationTypeDefinition',
83+
type: { kind: 'NamedType', name: { kind: 'Name', value: 'Query' } },
84+
operation: 'query',
85+
},
86+
],
87+
},
88+
],
89+
} as unknown as DocumentNode;

0 commit comments

Comments
 (0)