From 160a7530da91e64f3b965b38ebd9ed10c5001d26 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 30 Sep 2025 21:13:32 +1000 Subject: [PATCH 01/21] WIP use assignableTo to improve analysis speed --- ...getGraphQLObjectTypeResolversToGenerate.ts | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index 7a48b615..f9e632e0 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -20,12 +20,14 @@ export type GraphQLObjectTypeResolversToGenerate = Record< >; export const getGraphQLObjectTypeResolversToGenerate = ({ + mode, tsMorphProject, typesSourceFile, typeMappersMap, userDefinedSchemaObjectTypeMap, generatedTypesFileMeta, }: { + mode: 'smart' | 'experimental'; tsMorphProject: Project; typesSourceFile: SourceFile; typeMappersMap: TypeMappersMap; @@ -65,6 +67,156 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ {} ); + if (mode === 'experimental') { + // 1. Get property map of all schema types + const schemaResolversTypePropertyMap: Record = {}; + const resolverTypesMap: Record = {}; + const populateSchemaTypeResolversPropertyMap = ( + node: TypeAliasDeclaration | InterfaceDeclaration + ): void => { + const identifier = node.getNameNode(); + const identifierName = identifier.getText(); + + const schemaType = generatedSchemaTypeNameMap[identifierName]; + + if (schemaType && userDefinedSchemaObjectTypeMap[schemaType]) { + resolverTypesMap[schemaType] = { + name: identifierName, + node, + }; + } + }; + typesSourceFile + .getDescendantsOfKind(SyntaxKind.TypeAliasDeclaration) + .forEach(populateSchemaTypeResolversPropertyMap); + + typesSourceFile + .getDescendantsOfKind(SyntaxKind.InterfaceDeclaration) + .forEach(populateSchemaTypeResolversPropertyMap); + + // 3. Find resolvers to generate and add reason + const result: GraphQLObjectTypeResolversToGenerate = {}; + typeMappersEntries.forEach(([_, { schemaType, mapper }]) => { + const matchedSchemaTypePropertyMap = + schemaResolversTypePropertyMap[schemaType]; + if (!matchedSchemaTypePropertyMap) { + return; + } + + const originalDeclarationNode = mustGetMapperOriginalDeclarationNode({ + tsMorphProject, + mapper, + }); + const typeMapperPropertyMap = getNodePropertyMap({ + tsMorphProject, + node: originalDeclarationNode, + }); + + if (schemaType === 'Cat') { + const mapperProperties = originalDeclarationNode + .getType() + .getProperties(); + + mapperProperties.map((mapperSymbol) => { + const sourceType = mapperSymbol.getTypeAtLocation( + originalDeclarationNode + ); + resolverTypesMap[schemaType].node + .getType() + .getProperties() + .forEach((symbol) => { + // target is resolver + const resolverType = symbol.getTypeAtLocation( + resolverTypesMap[schemaType].node + ); + + const mainResolverType = resolverType.getNonNullableType(); + const targetType = mainResolverType + .getAliasTypeArguments()[0] + .getAliasTypeArguments()[0]; + + if (mapperSymbol.getName() === symbol.getName()) { + console.log('** Test:', { + mapperProp: mapperSymbol.getName(), + mapperType: sourceType.getText(), + resolverProp: symbol.getName(), + resolverType: targetType.getText(), + assignable: sourceType.isAssignableTo(targetType), + }); + } + + // console.log({ + // mapperProp: mapperSymbol.getName(), + // mapperType: sourceType.getText(), + // resolverProp: symbol.getName(), + // resolverType: targetType.getText(), + // isAssignable: typeChecker.isTypeAssignableTo( + // sourceType, + // targetType + // ), + // }); + }); + }); + } + + Object.values(matchedSchemaTypePropertyMap).forEach( + (schemaTypeProperty) => { + const typeMapperProperty = + typeMapperPropertyMap[schemaTypeProperty.name]; + + const typeMapperPropertyIdentifier = `${mapper.name}.${schemaTypeProperty.name}`; + const schemaTypePropertyIdentifier = `${schemaType}.${schemaTypeProperty.name}`; + + // Generated resolvers types may have one or more of these meta resolvers + // A mapper would most likely never have these resolvers, so we skip them + // Otherwise, these resolvers will always be generated + const metaResolvers: Record = { + __isTypeOf: true, + __resolveReference: true, + }; + + if (metaResolvers[schemaTypeProperty.name]) { + return; + } + + result[schemaType] = result[schemaType] || {}; + + // If mapper does not have a field in schema type, add missing resolver + if (!typeMapperProperty) { + result[schemaType][schemaTypeProperty.name] = { + resolverName: schemaTypeProperty.name, + resolverDeclaration: `async (_parent, _arg, _ctx) => { /* ${schemaTypePropertyIdentifier} resolver is required because ${schemaTypePropertyIdentifier} exists but ${typeMapperPropertyIdentifier} does not */ }`, + }; + return; + } + + /** + * FIXME: TypeScript's `isTypeAssignableTo` should be used to check if the mapper type vs resolver return type is compatible. + * The current challenge is to: + * - Switch from using the schema type to resolver return type e.g. `User` -> `UserResolver` + * - Take the ReturnType of the resolver function type e.g. `Resolver` + * + * For now, the workaround now is to generate all resolvers with matching names, + * then use TS diagnostics to see if there's error when trying to merge the two keys. + * + * Note: this happens only when mappers are used + */ + result[schemaType][schemaTypeProperty.name] = { + resolverName: schemaTypeProperty.name, + resolverDeclaration: `({ ${schemaTypeProperty.name} }, _arg, _ctx) => { + /* ${schemaTypePropertyIdentifier} resolver is required because ${schemaTypePropertyIdentifier} and ${typeMapperPropertyIdentifier} are not compatible */ + return ${schemaTypeProperty.name} + }`, + }; + + return; + } + ); + }); + + return result; + } + // 1. Get property map of all schema types const schemaResolversTypePropertyMap: Record = {}; From b70f3649708df3a7c8f99ad8635dd125dd8e0660 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sat, 11 Oct 2025 10:33:57 +1100 Subject: [PATCH 02/21] Set up base e2e for advanced static analysis --- .../project.json | 11 + .../codegen.ts | 23 ++ .../modules/base/schema.graphqls | 1 + .../modules/resolvers.generated.ts | 8 + .../modules/test/resolvers/Book.ts | 13 + .../modules/test/resolvers/User.ts | 37 +++ .../modules/test/schema.graphqls | 38 +++ .../modules/test/schema.mappers.ts | 32 ++ .../modules/types.generated.ts | 290 ++++++++++++++++++ 9 files changed, 453 insertions(+) create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/base/schema.graphqls create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/User.ts create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts diff --git a/packages/typescript-resolver-files-e2e/project.json b/packages/typescript-resolver-files-e2e/project.json index 13a7e887..1cd6442b 100644 --- a/packages/typescript-resolver-files-e2e/project.json +++ b/packages/typescript-resolver-files-e2e/project.json @@ -81,6 +81,13 @@ ], "parallel": false }, + "test-mappers-vs-schema-types-advanced": { + "commands": [ + "rimraf -g \"{projectRoot}/src/test-mappers-vs-schema-types-advanced/**/resolvers/\"", + "rimraf -g \"{projectRoot}/src/test-mappers-vs-schema-types-advanced/**/*.generated.*\"" + ], + "parallel": false + }, "test-config-ts": { "commands": [ "rimraf -g \"{projectRoot}/src/test-config-ts/**/resolvers/\"", @@ -179,6 +186,7 @@ "nx graphql-codegen typescript-resolver-files-e2e -c test-config-overrides --verbose", "nx graphql-codegen typescript-resolver-files-e2e -c test-mappers --verbose", "nx graphql-codegen typescript-resolver-files-e2e -c test-mappers-vs-schema-types --verbose", + "nx graphql-codegen typescript-resolver-files-e2e -c test-mappers-vs-schema-types-advanced --verbose", "nx graphql-codegen typescript-resolver-files-e2e -c test-config-ts --verbose", "nx graphql-codegen typescript-resolver-files-e2e -c test-modules-typedefs-file-mode --verbose", "nx graphql-codegen typescript-resolver-files-e2e -c test-modules-resolver-main-file-mode --verbose", @@ -231,6 +239,9 @@ "test-mappers-vs-schema-types": { "configFile": "{projectRoot}/src/test-mappers-vs-schema-types/codegen.ts" }, + "test-mappers-vs-schema-types-advanced": { + "configFile": "{projectRoot}/src/test-mappers-vs-schema-types-advanced/codegen.ts" + }, "test-config-ts": { "configFile": "{projectRoot}/src/test-config-ts/codegen.ts" }, diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts new file mode 100644 index 00000000..3e1c4e34 --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts @@ -0,0 +1,23 @@ +import type { CodegenConfig } from '@graphql-codegen/cli'; +import { defineConfig } from '@eddeee888/gcg-typescript-resolver-files'; + +const projectRoot = 'packages/typescript-resolver-files-e2e'; + +const config: CodegenConfig = { + schema: [ + `${projectRoot}/src/test-mappers-vs-schema-types-advanced/**/*.graphqls`, + ], + hooks: { afterAllFileWrite: ['prettier --write'] }, + generates: { + [`${projectRoot}/src/test-mappers-vs-schema-types-advanced/modules`]: + defineConfig({ + typeDefsFilePath: false, + mergeSchema: false, + resolverGeneration: 'minimal', + fixObjectTypeResolvers: 'experimental', + tsConfigFilePath: `${projectRoot}/tsconfig.lib.json`, + }), + }, +}; + +export default config; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/base/schema.graphqls b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/base/schema.graphqls new file mode 100644 index 00000000..a4dda08c --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/base/schema.graphqls @@ -0,0 +1 @@ +type Query diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts new file mode 100644 index 00000000..581ab159 --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts @@ -0,0 +1,8 @@ +/* This file was automatically generated. DO NOT UPDATE MANUALLY. */ +import type { Resolvers } from './types.generated'; +import { Book } from './test/resolvers/Book'; +import { User } from './test/resolvers/User'; +export const resolvers: Resolvers = { + Book: Book, + User: User, +}; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts new file mode 100644 index 00000000..b45f8e3f --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts @@ -0,0 +1,13 @@ +import type { BookResolvers } from './../../types.generated'; +/* + * Note: This object type is generated because "BookMapper" is declared. This is to ensure runtime safety. + * + * When a mapper is used, it is possible to hit runtime errors in some scenarios: + * - given a field name, the schema type's field type does not match mapper's field type + * - or a schema type's field does not exist in the mapper's fields + * + * If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config. + */ +export const Book: BookResolvers = { + /* Implement Book resolver logic here */ +}; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/User.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/User.ts new file mode 100644 index 00000000..13b36bcb --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/User.ts @@ -0,0 +1,37 @@ +import type { UserResolvers } from './../../types.generated'; +/* + * Note: This object type is generated because "UserMapper" is declared. This is to ensure runtime safety. + * + * When a mapper is used, it is possible to hit runtime errors in some scenarios: + * - given a field name, the schema type's field type does not match mapper's field type + * - or a schema type's field does not exist in the mapper's fields + * + * If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config. + */ +export const User: UserResolvers = { + /* Implement User resolver logic here */ + mmBook1: ({ mmBook1 }, _arg, _ctx) => { + /* User.mmBook1 resolver is required because User.mmBook1 and UserMapper.mmBook1 are not compatible */ + return mmBook1; + }, + mmBook2: ({ mmBook2 }, _arg, _ctx) => { + /* User.mmBook2 resolver is required because User.mmBook2 and UserMapper.mmBook2 are not compatible */ + return mmBook2; + }, + mmBooks1: ({ mmBooks1 }, _arg, _ctx) => { + /* User.mmBooks1 resolver is required because User.mmBooks1 and UserMapper.mmBooks1 are not compatible */ + return mmBooks1; + }, + mmBooks2: ({ mmBooks2 }, _arg, _ctx) => { + /* User.mmBooks2 resolver is required because User.mmBooks2 and UserMapper.mmBooks2 are not compatible */ + return mmBooks2; + }, + mmBooks3: ({ mmBooks3 }, _arg, _ctx) => { + /* User.mmBooks3 resolver is required because User.mmBooks3 and UserMapper.mmBooks3 are not compatible */ + return mmBooks3; + }, + mmBooks4: ({ mmBooks4 }, _arg, _ctx) => { + /* User.mmBooks4 resolver is required because User.mmBooks4 and UserMapper.mmBooks4 are not compatible */ + return mmBooks4; + }, +}; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls new file mode 100644 index 00000000..2658d121 --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls @@ -0,0 +1,38 @@ +extend type Base { + user: User +} + +type User { + id: ID! + + # Matches + book: Book + bookNonNull: Book! + bookInline: Book! + + ## mappers fields use Array + books1: [Book] + books2: [Book!] + books3: [Book]! + books4: [Book!]! + + ## mappers fields use T[] + books5: [Book] + books6: [Book!] + books7: [Book]! + books8: [Book!]! + + # Mismatches + mmBook1: Book + mmBook2: Book! + + mmBooks1: [Book] + mmBooks2: [Book!] + mmBooks3: [Book]! + mmBooks4: [Book!]! +} + +type Book { + id: ID! + name: String! +} diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts new file mode 100644 index 00000000..574e4ca6 --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts @@ -0,0 +1,32 @@ +export type UserMapper = { + id: number; + + // Matches + book: BookMapper | null | undefined; + bookNonNull: BookMapper; + bookInline: { id: number; name: string }; + + books1: Array | null; + books2: Array | null; + books3: Array; + books4: Array; + + books5: (BookMapper | null)[] | null; + books6: BookMapper[] | null; + books7: (BookMapper | null)[]; + books8: BookMapper[]; + + // Mismatches + mmBook1: { id: string }; + mmBook2: BookMapper | null; + + mmBooks1: Array | boolean; + mmBooks2: Array | null; + mmBooks3: Array | null; + mmBooks4: Array; +}; + +export type BookMapper = { + id: number; + name: string; +}; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts new file mode 100644 index 00000000..0f2135c1 --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts @@ -0,0 +1,290 @@ +import { GraphQLResolveInfo } from 'graphql'; +import { BookMapper, UserMapper } from './test/schema.mappers'; +export type Maybe = T | null | undefined; +export type InputMaybe = T | null | undefined; +export type Exact = { + [K in keyof T]: T[K]; +}; +export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; +}; +export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; +}; +export type MakeEmpty< + T extends { [key: string]: unknown }, + K extends keyof T +> = { [_ in K]?: never }; +export type Incremental = + | T + | { + [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; + }; +export type Omit = Pick>; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string | number }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; +}; + +export type Base = { + __typename?: 'Base'; + user?: Maybe; +}; + +export type Book = { + __typename?: 'Book'; + id: Scalars['ID']['output']; + name: Scalars['String']['output']; +}; + +export type Query = { + __typename?: 'Query'; +}; + +export type User = { + __typename?: 'User'; + book?: Maybe; + bookInline: Book; + bookNonNull: Book; + books1?: Maybe>>; + books2?: Maybe>; + books3: Array>; + books4: Array; + books5?: Maybe>>; + books6?: Maybe>; + books7: Array>; + books8: Array; + id: Scalars['ID']['output']; + mmBook1?: Maybe; + mmBook2: Book; + mmBooks1?: Maybe>>; + mmBooks2?: Maybe>; + mmBooks3: Array>; + mmBooks4: Array; +}; + +export type ResolverTypeWrapper = Promise | T; + +export type ResolverWithResolve = { + resolve: ResolverFn; +}; +export type Resolver< + TResult, + TParent = Record, + TContext = Record, + TArgs = Record +> = + | ResolverFn + | ResolverWithResolve; + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => Promise | TResult; + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => AsyncIterable | Promise>; + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +export interface SubscriptionSubscriberObject< + TResult, + TKey extends string, + TParent, + TContext, + TArgs +> { + subscribe: SubscriptionSubscribeFn< + { [key in TKey]: TResult }, + TParent, + TContext, + TArgs + >; + resolve?: SubscriptionResolveFn< + TResult, + { [key in TKey]: TResult }, + TContext, + TArgs + >; +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn; + resolve: SubscriptionResolveFn; +} + +export type SubscriptionObject< + TResult, + TKey extends string, + TParent, + TContext, + TArgs +> = + | SubscriptionSubscriberObject + | SubscriptionResolverObject; + +export type SubscriptionResolver< + TResult, + TKey extends string, + TParent = Record, + TContext = Record, + TArgs = Record +> = + | (( + ...args: any[] + ) => SubscriptionObject) + | SubscriptionObject; + +export type TypeResolveFn< + TTypes, + TParent = Record, + TContext = Record +> = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo +) => Maybe | Promise>; + +export type IsTypeOfResolverFn< + T = Record, + TContext = Record +> = ( + obj: T, + context: TContext, + info: GraphQLResolveInfo +) => boolean | Promise; + +export type NextResolverFn = () => Promise; + +export type DirectiveResolverFn< + TResult = Record, + TParent = Record, + TContext = Record, + TArgs = Record +> = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo +) => TResult | Promise; + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Base: ResolverTypeWrapper< + Omit & { user?: Maybe } + >; + Book: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + String: ResolverTypeWrapper; + Query: ResolverTypeWrapper>; + User: ResolverTypeWrapper; + Boolean: ResolverTypeWrapper; +}; + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Base: Omit & { user?: Maybe }; + Book: BookMapper; + ID: Scalars['ID']['output']; + String: Scalars['String']['output']; + Query: Record; + User: UserMapper; + Boolean: Scalars['Boolean']['output']; +}; + +export type BaseResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['Base'] = ResolversParentTypes['Base'] +> = { + user?: Resolver, ParentType, ContextType>; +}; + +export type BookResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book'] +> = { + id?: Resolver; + name?: Resolver; +}; + +export type UserResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] +> = { + book?: Resolver, ParentType, ContextType>; + bookInline?: Resolver; + bookNonNull?: Resolver; + books1?: Resolver< + Maybe>>, + ParentType, + ContextType + >; + books2?: Resolver< + Maybe>, + ParentType, + ContextType + >; + books3?: Resolver< + Array>, + ParentType, + ContextType + >; + books4?: Resolver, ParentType, ContextType>; + books5?: Resolver< + Maybe>>, + ParentType, + ContextType + >; + books6?: Resolver< + Maybe>, + ParentType, + ContextType + >; + books7?: Resolver< + Array>, + ParentType, + ContextType + >; + books8?: Resolver, ParentType, ContextType>; + id?: Resolver; + mmBook1?: Resolver, ParentType, ContextType>; + mmBook2?: Resolver; + mmBooks1?: Resolver< + Maybe>>, + ParentType, + ContextType + >; + mmBooks2?: Resolver< + Maybe>, + ParentType, + ContextType + >; + mmBooks3?: Resolver< + Array>, + ParentType, + ContextType + >; + mmBooks4?: Resolver, ParentType, ContextType>; +}; + +export type Resolvers = { + Base?: BaseResolvers; + Book?: BookResolvers; + User?: UserResolvers; +}; From 0917eadcb7883b3ed9c937636d4372a602e586ce Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sat, 11 Oct 2025 15:52:42 +1100 Subject: [PATCH 03/21] Wire in experimental flag --- .../typescript-resolver-files/src/preset.ts | 9 +++--- .../validatePresetConfig.ts | 32 ++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/packages/typescript-resolver-files/src/preset.ts b/packages/typescript-resolver-files/src/preset.ts index b3de1b6b..cba4ddd8 100644 --- a/packages/typescript-resolver-files/src/preset.ts +++ b/packages/typescript-resolver-files/src/preset.ts @@ -165,16 +165,17 @@ export const preset: Types.OutputPreset = { const graphQLObjectTypeResolversToGenerate = await profiler.run( async () => - fixObjectTypeResolvers.object === 'smart' - ? getGraphQLObjectTypeResolversToGenerate({ + fixObjectTypeResolvers.object === 'disabled' + ? {} + : getGraphQLObjectTypeResolversToGenerate({ + mode: fixObjectTypeResolvers.object, tsMorphProject, typesSourceFile, userDefinedSchemaObjectTypeMap: mergedConfig.userDefinedSchemaTypeMap.object, typeMappersMap, generatedTypesFileMeta, - }) - : {}, + }), createProfilerRunName('getGraphQLObjectTypeResolversToGenerate') ); diff --git a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts index 3c854934..a75980f5 100644 --- a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts +++ b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts @@ -25,10 +25,10 @@ type ParsedTypesPluginsConfig = Omit< type ConfigMode = 'merged' | 'modules'; type ResolverMainFileMode = 'merged' | 'modules'; export type TypeDefsFileMode = 'merged' | 'mergedWhitelisted' | 'modules'; -type StringFixObjectTypeResolvers = 'smart' | 'disabled'; +type StringFixObjectTypeResolvers = 'smart' | 'experimental' | 'disabled'; type NormalizedFixObjectTypeResolvers = { - object: 'smart' | 'disabled'; - enum: 'smart' | 'disabled'; + object: 'smart' | 'experimental' | 'disabled'; + enum: 'smart' | 'experimental' | 'disabled'; }; type StringResolverGeneration = 'disabled' | 'recommended' | 'minimal' | 'all'; type NormalizedResolverGeneration = { @@ -144,7 +144,7 @@ export const validatePresetConfig = ({ externalResolvers = {}, typesPluginsConfig = {}, tsConfigFilePath = './tsconfig.json', - fixObjectTypeResolvers = 'smart', + fixObjectTypeResolvers = 'experimental', emitLegacyCommonJSImports = true, }: RawPresetConfig): ParsedPresetConfig => { if (mode !== 'merged' && mode !== 'modules') { @@ -159,11 +159,12 @@ export const validatePresetConfig = ({ if ( typeof fixObjectTypeResolvers !== 'object' && fixObjectTypeResolvers !== 'smart' && + fixObjectTypeResolvers !== 'experimental' && fixObjectTypeResolvers !== 'disabled' ) { throw new Error( fmt.error( - 'presetConfig.fixObjectTypeResolvers must be an object, "smart" or "disabled" (default is "smart")', + 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "experimental" or "disabled" (default is "experimental")', 'Validation' ) ); @@ -422,24 +423,19 @@ const parseResolverGeneration = ( const parseFixObjectTypeResolvers = ( fixObjectTypeResolvers: StringFixObjectTypeResolvers | Record ): NormalizedFixObjectTypeResolvers => { - if (fixObjectTypeResolvers === 'smart') { + if (typeof fixObjectTypeResolvers === 'string') { return { - object: 'smart', - enum: 'smart', + object: fixObjectTypeResolvers, + enum: fixObjectTypeResolvers, }; } - if (fixObjectTypeResolvers === 'disabled') { - return { - object: 'disabled', - enum: 'disabled', + const allowedOptions: Record = + { + smart: 'smart', + disabled: 'disabled', + experimental: 'experimental', }; - } - - const allowedOptions: Record = { - smart: 'smart', - disabled: 'disabled', - }; return { object: allowedOptions[fixObjectTypeResolvers.object] || 'disabled', From 023c87d67932437be9813b11cf5b044ab989352d Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sat, 11 Oct 2025 16:23:58 +1100 Subject: [PATCH 04/21] Update test to check for overwritten --- .../typescript-resolver-files-e2e/project.json | 4 +++- .../modules/test/resolvers/Book.ts | 15 +++++---------- .../modules/test/schema.graphqls | 1 + .../modules/test/schema.mappers.ts | 3 ++- .../modules/types.generated.ts | 6 ++++++ .../testSetup.ts | 18 ++++++++++++++++++ 6 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts diff --git a/packages/typescript-resolver-files-e2e/project.json b/packages/typescript-resolver-files-e2e/project.json index 1cd6442b..637ff848 100644 --- a/packages/typescript-resolver-files-e2e/project.json +++ b/packages/typescript-resolver-files-e2e/project.json @@ -26,6 +26,7 @@ "rimraf -g \"{projectRoot}/src/**/*.gen.*\"", "rimraf -g \"{projectRoot}/src/**/generated\"", "tsx --tsconfig={projectRoot}/tsconfig.json {projectRoot}/src/test-mappers-vs-schema-types/testSetup.ts", + "tsx --tsconfig={projectRoot}/tsconfig.json {projectRoot}/src/test-mappers-vs-schema-types-advanced/testSetup.ts", "tsx --tsconfig={projectRoot}/tsconfig.json {projectRoot}/src/test-resolvers-auto-wireup/testSetup.ts" ], "parallel": false @@ -84,7 +85,8 @@ "test-mappers-vs-schema-types-advanced": { "commands": [ "rimraf -g \"{projectRoot}/src/test-mappers-vs-schema-types-advanced/**/resolvers/\"", - "rimraf -g \"{projectRoot}/src/test-mappers-vs-schema-types-advanced/**/*.generated.*\"" + "rimraf -g \"{projectRoot}/src/test-mappers-vs-schema-types-advanced/**/*.generated.*\"", + "tsx --tsconfig={projectRoot}/tsconfig.json {projectRoot}/src/test-mappers-vs-schema-types-advanced/testSetup.ts" ], "parallel": false }, diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts index b45f8e3f..87fe6c6b 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts @@ -1,13 +1,8 @@ import type { BookResolvers } from './../../types.generated'; -/* - * Note: This object type is generated because "BookMapper" is declared. This is to ensure runtime safety. - * - * When a mapper is used, it is possible to hit runtime errors in some scenarios: - * - given a field name, the schema type's field type does not match mapper's field type - * - or a schema type's field does not exist in the mapper's fields - * - * If you want to skip this file generation, remove the mapper or update the pattern in the `resolverGeneration.object` config. - */ +/* This file has been created on filesystem by @workspace/testing#createTestSetup */ + export const Book: BookResolvers = { - /* Implement Book resolver logic here */ + relatedBooks: ({ relatedBooks }) => { + /* This existing content is not overwritten*/ + }, }; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls index 2658d121..8c6dd344 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls @@ -35,4 +35,5 @@ type User { type Book { id: ID! name: String! + relatedBooks: [Book!]! } diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts index 574e4ca6..6e5ca239 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts @@ -4,7 +4,7 @@ export type UserMapper = { // Matches book: BookMapper | null | undefined; bookNonNull: BookMapper; - bookInline: { id: number; name: string }; + bookInline: { id: number; name: string; relatedBooks: { id: number }[] }; // Inline version of BookMapper. Must keep in sync. books1: Array | null; books2: Array | null; @@ -29,4 +29,5 @@ export type UserMapper = { export type BookMapper = { id: number; name: string; + relatedBooks: { id: number }[]; }; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts index 0f2135c1..4ea228fe 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts @@ -39,6 +39,7 @@ export type Book = { __typename?: 'Book'; id: Scalars['ID']['output']; name: Scalars['String']['output']; + relatedBooks: Array; }; export type Query = { @@ -221,6 +222,11 @@ export type BookResolvers< > = { id?: Resolver; name?: Resolver; + relatedBooks?: Resolver< + Array, + ParentType, + ContextType + >; }; export type UserResolvers< diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts new file mode 100644 index 00000000..64dce5ca --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts @@ -0,0 +1,18 @@ +import { createTestSetup } from '@workspace/testing'; + +const projectRoot = 'packages/typescript-resolver-files-e2e'; + +createTestSetup({ + baseDir: `${projectRoot}/src/test-mappers-vs-schema-types-advanced/`, + files: [ + { + file: 'modules/test/resolvers/Book.ts', + content: ` + export const Book: BookResolvers = { + relatedBooks: ({ relatedBooks }) => { + /* This existing content is not overwritten*/ + }, + };`, + }, + ], +}); From 893d4993bf15dad9c35092a34a44b004d4be5abd Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sat, 11 Oct 2025 23:56:26 +1100 Subject: [PATCH 05/21] Implement fast typechecks --- .../codegen.ts | 2 +- ...PropertyAssignmentNodesIfNotImplemented.ts | 6 +- .../generateResolverFiles/postProcessFiles.ts | 7 +- ...getGraphQLObjectTypeResolversToGenerate.ts | 173 +++++++----------- .../getNodePropertyMap.ts | 11 +- .../validatePresetConfig.ts | 23 ++- 6 files changed, 96 insertions(+), 126 deletions(-) diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts index 3e1c4e34..05d244ca 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/codegen.ts @@ -14,7 +14,7 @@ const config: CodegenConfig = { typeDefsFilePath: false, mergeSchema: false, resolverGeneration: 'minimal', - fixObjectTypeResolvers: 'experimental', + fixObjectTypeResolvers: 'fast', tsConfigFilePath: `${projectRoot}/tsconfig.lib.json`, }), }, diff --git a/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts b/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts index 78b59642..8c7d3d10 100644 --- a/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts +++ b/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts @@ -21,10 +21,12 @@ export const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented = ({ addedPropertyAssignmentNodes, sourceFile, resolverFile, + mode, }: { addedPropertyAssignmentNodes: AddedPropertyAssignmentNodes; sourceFile: SourceFile; resolverFile: ObjectTypeFile; + mode: 'smart' | 'fast'; }): void => { const sourceFilePath = sourceFile.getFilePath().toString(); addedPropertyAssignmentNodes[sourceFilePath] = @@ -129,7 +131,9 @@ export const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented = ({ ] = { node: addedNode, resolverFile, - __toBeRemoved: true, + // When mode is "smart", we use TS compiler for typechecking, and it'd remove the node if there is no compilation error. Therefore, `__toBeRemoved: true` + // When mode is "experimental", we already check whether the the type is assignable, so no need to remove the node. + __toBeRemoved: mode === 'smart' ? true : false, }; } ); diff --git a/packages/typescript-resolver-files/src/generateResolverFiles/postProcessFiles.ts b/packages/typescript-resolver-files/src/generateResolverFiles/postProcessFiles.ts index 34ffbb4e..ed3558b7 100644 --- a/packages/typescript-resolver-files/src/generateResolverFiles/postProcessFiles.ts +++ b/packages/typescript-resolver-files/src/generateResolverFiles/postProcessFiles.ts @@ -85,18 +85,21 @@ export const postProcessFiles = ({ } if ( - fixObjectTypeResolvers.object === 'smart' && + (fixObjectTypeResolvers.object === 'smart' || + fixObjectTypeResolvers.object === 'fast') && resolverFile.__filetype === 'objectType' ) { addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ addedPropertyAssignmentNodes, sourceFile, resolverFile, + mode: fixObjectTypeResolvers.object, }); } if ( - fixObjectTypeResolvers.enum === 'smart' && + (fixObjectTypeResolvers.enum === 'smart' || + fixObjectTypeResolvers.enum === 'fast') && resolverFile.__filetype === 'enumResolver' ) { ensureEnumTypeResolversAreGenerated(sourceFile, resolverFile); diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index f9e632e0..6009ea4d 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -27,7 +27,7 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ userDefinedSchemaObjectTypeMap, generatedTypesFileMeta, }: { - mode: 'smart' | 'experimental'; + mode: 'smart' | 'fast'; tsMorphProject: Project; typesSourceFile: SourceFile; typeMappersMap: TypeMappersMap; @@ -67,25 +67,33 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ {} ); - if (mode === 'experimental') { + if (mode === 'fast') { // 1. Get property map of all schema types - const schemaResolversTypePropertyMap: Record = {}; - const resolverTypesMap: Record = {}; + const resolverTypesMap: Record< + string, + { + node: Node; + properties: NodePropertyMap; + } + > = {}; const populateSchemaTypeResolversPropertyMap = ( node: TypeAliasDeclaration | InterfaceDeclaration ): void => { - const identifier = node.getNameNode(); - const identifierName = identifier.getText(); + const identifierName = node.getNameNode().getText(); // e.g. UserResolvers, BookResolvers - const schemaType = generatedSchemaTypeNameMap[identifierName]; + const schemaType = generatedSchemaTypeNameMap[identifierName]; // schemaType examples: User, Book if (schemaType && userDefinedSchemaObjectTypeMap[schemaType]) { resolverTypesMap[schemaType] = { - name: identifierName, node, + properties: getNodePropertyMap({ + tsMorphProject, + node, + }), }; } }; + typesSourceFile .getDescendantsOfKind(SyntaxKind.TypeAliasDeclaration) .forEach(populateSchemaTypeResolversPropertyMap); @@ -97,121 +105,70 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ // 3. Find resolvers to generate and add reason const result: GraphQLObjectTypeResolversToGenerate = {}; typeMappersEntries.forEach(([_, { schemaType, mapper }]) => { - const matchedSchemaTypePropertyMap = - schemaResolversTypePropertyMap[schemaType]; - if (!matchedSchemaTypePropertyMap) { + const resolverType = resolverTypesMap[schemaType]; + if (!resolverType) { return; } - const originalDeclarationNode = mustGetMapperOriginalDeclarationNode({ - tsMorphProject, - mapper, - }); - const typeMapperPropertyMap = getNodePropertyMap({ + const mapperOriginalDeclarationNode = + mustGetMapperOriginalDeclarationNode({ + tsMorphProject, + mapper, + }); + const mapperPropsMap = getNodePropertyMap({ tsMorphProject, - node: originalDeclarationNode, + node: mapperOriginalDeclarationNode, }); - if (schemaType === 'Cat') { - const mapperProperties = originalDeclarationNode - .getType() - .getProperties(); - - mapperProperties.map((mapperSymbol) => { - const sourceType = mapperSymbol.getTypeAtLocation( - originalDeclarationNode - ); - resolverTypesMap[schemaType].node - .getType() - .getProperties() - .forEach((symbol) => { - // target is resolver - const resolverType = symbol.getTypeAtLocation( - resolverTypesMap[schemaType].node - ); - - const mainResolverType = resolverType.getNonNullableType(); - const targetType = mainResolverType - .getAliasTypeArguments()[0] - .getAliasTypeArguments()[0]; - - if (mapperSymbol.getName() === symbol.getName()) { - console.log('** Test:', { - mapperProp: mapperSymbol.getName(), - mapperType: sourceType.getText(), - resolverProp: symbol.getName(), - resolverType: targetType.getText(), - assignable: sourceType.isAssignableTo(targetType), - }); - } - - // console.log({ - // mapperProp: mapperSymbol.getName(), - // mapperType: sourceType.getText(), - // resolverProp: symbol.getName(), - // resolverType: targetType.getText(), - // isAssignable: typeChecker.isTypeAssignableTo( - // sourceType, - // targetType - // ), - // }); - }); - }); - } + Object.values(resolverType.properties).forEach((resolverTypeProp) => { + result[schemaType] = result[schemaType] || {}; + const resolverTypePropName = resolverTypeProp.name; - Object.values(matchedSchemaTypePropertyMap).forEach( - (schemaTypeProperty) => { - const typeMapperProperty = - typeMapperPropertyMap[schemaTypeProperty.name]; + const mapperPropIdentifier = `${mapper.name}.${resolverTypePropName}`; + const schemaTypePropIdentifier = `${schemaType}.${resolverTypePropName}`; - const typeMapperPropertyIdentifier = `${mapper.name}.${schemaTypeProperty.name}`; - const schemaTypePropertyIdentifier = `${schemaType}.${schemaTypeProperty.name}`; + // 1. Generated resolvers types may have one or more of these meta resolvers + // A mapper would most likely never have these resolvers, so we skip them + // Otherwise, these resolvers will always be generated + const metaResolvers: Record = { + __isTypeOf: true, + __resolveReference: true, + }; + if (metaResolvers[resolverTypePropName]) { + return; + } - // Generated resolvers types may have one or more of these meta resolvers - // A mapper would most likely never have these resolvers, so we skip them - // Otherwise, these resolvers will always be generated - const metaResolvers: Record = { - __isTypeOf: true, - __resolveReference: true, + const mapperProp = mapperPropsMap[resolverTypePropName]; + // 2. If mapper does not have a field in resolver type, add missing resolver + if (!mapperProp) { + result[schemaType][resolverTypePropName] = { + resolverName: resolverTypePropName, + resolverDeclaration: `async (_parent, _arg, _ctx) => { /* ${schemaTypePropIdentifier} resolver is required because ${schemaTypePropIdentifier} exists but ${mapperPropIdentifier} does not */ }`, }; + return; + } - if (metaResolvers[schemaTypeProperty.name]) { - return; - } - - result[schemaType] = result[schemaType] || {}; - - // If mapper does not have a field in schema type, add missing resolver - if (!typeMapperProperty) { - result[schemaType][schemaTypeProperty.name] = { - resolverName: schemaTypeProperty.name, - resolverDeclaration: `async (_parent, _arg, _ctx) => { /* ${schemaTypePropertyIdentifier} resolver is required because ${schemaTypePropertyIdentifier} exists but ${typeMapperPropertyIdentifier} does not */ }`, - }; - return; - } + // 3. If mapper and resolver props have the same name, compare their types + const sourceType = mapperProp.type; - /** - * FIXME: TypeScript's `isTypeAssignableTo` should be used to check if the mapper type vs resolver return type is compatible. - * The current challenge is to: - * - Switch from using the schema type to resolver return type e.g. `User` -> `UserResolver` - * - Take the ReturnType of the resolver function type e.g. `Resolver` - * - * For now, the workaround now is to generate all resolvers with matching names, - * then use TS diagnostics to see if there's error when trying to merge the two keys. - * - * Note: this happens only when mappers are used - */ - result[schemaType][schemaTypeProperty.name] = { - resolverName: schemaTypeProperty.name, - resolverDeclaration: `({ ${schemaTypeProperty.name} }, _arg, _ctx) => { - /* ${schemaTypePropertyIdentifier} resolver is required because ${schemaTypePropertyIdentifier} and ${typeMapperPropertyIdentifier} are not compatible */ - return ${schemaTypeProperty.name} - }`, - }; + // A resolver type may look like this: + // `book?: Resolver, ParentType, ContextType>;` + const targetType = resolverTypeProp.type // type is `Resolver, ParentType, ContextType> | undefined` because book resolver is optional + .getNonNullableType() // removing nullable, it becomes `Resolver, ParentType, ContextType>` + .getAliasTypeArguments()[0]; // first type argument is `Maybe` + if (sourceType.isAssignableTo(targetType)) { return; } - ); + + result[schemaType][resolverTypePropName] = { + resolverName: resolverTypePropName, + resolverDeclaration: `({ ${resolverTypePropName} }, _arg, _ctx) => { + /* ${schemaTypePropIdentifier} resolver is required because ${schemaTypePropIdentifier} and ${mapperPropIdentifier} are not compatible */ + return ${resolverTypePropName} + }`, + }; + }); }); return result; diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts index 8fc50381..fba42d30 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts @@ -2,10 +2,14 @@ import { type Project, type ClassDeclaration, type Node, + type Type, SyntaxKind, } from 'ts-morph'; -type NodePropertyMapValue = { name: string }; +type NodePropertyMapValue = { + name: string; + type: Type; +}; export type NodePropertyMap = Record; /** @@ -38,14 +42,16 @@ export const getNodePropertyMap = ({ .map((prop) => { return { name: prop.getName(), + type: prop.getTypeAtLocation(node), }; }); })(); const nodePropertyMap = properties.reduce( - (res, { name }) => { + (res, { name, type }) => { res[name] = { name, + type, }; return res; }, @@ -81,6 +87,7 @@ const collectClassNodeProperties = ( } result.push({ name: prop.getName(), + type: prop.getType(), }); }); }; diff --git a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts index a75980f5..0ba72a9d 100644 --- a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts +++ b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts @@ -25,10 +25,10 @@ type ParsedTypesPluginsConfig = Omit< type ConfigMode = 'merged' | 'modules'; type ResolverMainFileMode = 'merged' | 'modules'; export type TypeDefsFileMode = 'merged' | 'mergedWhitelisted' | 'modules'; -type StringFixObjectTypeResolvers = 'smart' | 'experimental' | 'disabled'; +type StringFixObjectTypeResolvers = 'smart' | 'fast' | 'disabled'; type NormalizedFixObjectTypeResolvers = { - object: 'smart' | 'experimental' | 'disabled'; - enum: 'smart' | 'experimental' | 'disabled'; + object: 'smart' | 'fast' | 'disabled'; + enum: 'smart' | 'fast' | 'disabled'; }; type StringResolverGeneration = 'disabled' | 'recommended' | 'minimal' | 'all'; type NormalizedResolverGeneration = { @@ -144,7 +144,7 @@ export const validatePresetConfig = ({ externalResolvers = {}, typesPluginsConfig = {}, tsConfigFilePath = './tsconfig.json', - fixObjectTypeResolvers = 'experimental', + fixObjectTypeResolvers = 'fast', emitLegacyCommonJSImports = true, }: RawPresetConfig): ParsedPresetConfig => { if (mode !== 'merged' && mode !== 'modules') { @@ -159,12 +159,12 @@ export const validatePresetConfig = ({ if ( typeof fixObjectTypeResolvers !== 'object' && fixObjectTypeResolvers !== 'smart' && - fixObjectTypeResolvers !== 'experimental' && + fixObjectTypeResolvers !== 'fast' && fixObjectTypeResolvers !== 'disabled' ) { throw new Error( fmt.error( - 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "experimental" or "disabled" (default is "experimental")', + 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "fast")', 'Validation' ) ); @@ -430,12 +430,11 @@ const parseFixObjectTypeResolvers = ( }; } - const allowedOptions: Record = - { - smart: 'smart', - disabled: 'disabled', - experimental: 'experimental', - }; + const allowedOptions: Record = { + smart: 'smart', + fast: 'fast', + disabled: 'disabled', + }; return { object: allowedOptions[fixObjectTypeResolvers.object] || 'disabled', From c3c9ce6e49736393ab44cf2084da0fdfc3f10fc3 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 00:11:57 +1100 Subject: [PATCH 06/21] Add test case for advanced comparison --- .../modules/test/resolvers/Book.ts | 4 ++++ .../modules/test/schema.graphqls | 1 + .../modules/test/schema.mappers.ts | 8 +++++++- .../modules/types.generated.ts | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts index 87fe6c6b..c0090077 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts @@ -5,4 +5,8 @@ export const Book: BookResolvers = { relatedBooks: ({ relatedBooks }) => { /* This existing content is not overwritten*/ }, + nextBookInSeries: ({ nextBookInSeries }, _arg, _ctx) => { + /* Book.nextBookInSeries resolver is required because Book.nextBookInSeries and BookMapper.nextBookInSeries are not compatible */ + return nextBookInSeries; + }, }; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls index 8c6dd344..b46a0238 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls @@ -36,4 +36,5 @@ type Book { id: ID! name: String! relatedBooks: [Book!]! + nextBookInSeries: Book! } diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts index 6e5ca239..b3112464 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.mappers.ts @@ -4,7 +4,12 @@ export type UserMapper = { // Matches book: BookMapper | null | undefined; bookNonNull: BookMapper; - bookInline: { id: number; name: string; relatedBooks: { id: number }[] }; // Inline version of BookMapper. Must keep in sync. + bookInline: { + id: number; + name: string; + relatedBooks: { id: number }[]; + nextBookInSeries: { id: number }; + }; // Inline version of BookMapper. Must keep in sync. books1: Array | null; books2: Array | null; @@ -30,4 +35,5 @@ export type BookMapper = { id: number; name: string; relatedBooks: { id: number }[]; + nextBookInSeries: { id: number }; }; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts index 4ea228fe..84c2d602 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts @@ -39,6 +39,7 @@ export type Book = { __typename?: 'Book'; id: Scalars['ID']['output']; name: Scalars['String']['output']; + nextBookInSeries: Book; relatedBooks: Array; }; @@ -222,6 +223,7 @@ export type BookResolvers< > = { id?: Resolver; name?: Resolver; + nextBookInSeries?: Resolver; relatedBooks?: Resolver< Array, ParentType, From 58f62a89920e73f4ee9a7c38e54b4a6be743f6ea Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 21:27:49 +1100 Subject: [PATCH 07/21] Fix issue where added nodes is removed whilst codemod in fast mode --- ...ypeResolversPropertyAssignmentNodesIfNotImplemented.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts b/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts index 8c7d3d10..d00719f6 100644 --- a/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts +++ b/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.ts @@ -126,14 +126,18 @@ export const addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented = ({ initializer: resolverDeclaration, }); + if (mode === 'fast') { + resolverFile.filesystem.contentUpdated = true; + } + addedPropertyAssignmentNodes[sourceFilePath][ addedNode.getStartLineNumber() ] = { node: addedNode, resolverFile, // When mode is "smart", we use TS compiler for typechecking, and it'd remove the node if there is no compilation error. Therefore, `__toBeRemoved: true` - // When mode is "experimental", we already check whether the the type is assignable, so no need to remove the node. - __toBeRemoved: mode === 'smart' ? true : false, + // When mode is "fast", we already check whether the the type is assignable, so no need to remove the node. + __toBeRemoved: mode === 'fast' ? false : true, }; } ); From e3fbbd9d4a76f7c719e6528a6aec6b501c5f955d Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 21:38:11 +1100 Subject: [PATCH 08/21] Fix test setup to test contentUpdated --- .../modules/test/resolvers/Book.ts | 2 -- .../src/test-mappers-vs-schema-types-advanced/testSetup.ts | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts index c0090077..9f3192b4 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Book.ts @@ -1,6 +1,4 @@ import type { BookResolvers } from './../../types.generated'; -/* This file has been created on filesystem by @workspace/testing#createTestSetup */ - export const Book: BookResolvers = { relatedBooks: ({ relatedBooks }) => { /* This existing content is not overwritten*/ diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts index 64dce5ca..cc100a7c 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/testSetup.ts @@ -8,6 +8,7 @@ createTestSetup({ { file: 'modules/test/resolvers/Book.ts', content: ` + import type { BookResolvers } from './../../types.generated'; export const Book: BookResolvers = { relatedBooks: ({ relatedBooks }) => { /* This existing content is not overwritten*/ From a1cead51dc077ce9b0d8cd68645bfbfb67e81f13 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 21:41:46 +1100 Subject: [PATCH 09/21] Make fast mode opt-in --- .../src/validatePresetConfig/validatePresetConfig.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts index 0ba72a9d..383a7490 100644 --- a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts +++ b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts @@ -144,7 +144,7 @@ export const validatePresetConfig = ({ externalResolvers = {}, typesPluginsConfig = {}, tsConfigFilePath = './tsconfig.json', - fixObjectTypeResolvers = 'fast', + fixObjectTypeResolvers = 'smart', emitLegacyCommonJSImports = true, }: RawPresetConfig): ParsedPresetConfig => { if (mode !== 'merged' && mode !== 'modules') { @@ -164,7 +164,7 @@ export const validatePresetConfig = ({ ) { throw new Error( fmt.error( - 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "fast")', + 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "smart")', 'Validation' ) ); From f39ed28f5ee2ad56c612d7df1a9ee83b5f56055b Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 22:04:11 +1100 Subject: [PATCH 10/21] Add changeset --- .changeset/shaky-llamas-scream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaky-llamas-scream.md diff --git a/.changeset/shaky-llamas-scream.md b/.changeset/shaky-llamas-scream.md new file mode 100644 index 00000000..d46358fd --- /dev/null +++ b/.changeset/shaky-llamas-scream.md @@ -0,0 +1,5 @@ +--- +'@eddeee888/gcg-typescript-resolver-files': minor +--- + +Add fixObjectTypeResolvers 'fast' mode (experimental) From c0519d949106179befed5580f9769994dd5f3de2 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 22:35:37 +1100 Subject: [PATCH 11/21] Fix tests --- ...rtyAssignmentNodesIfNotImplemented.spec.ts | 587 +++++++++++++++++- .../validatePresetConfig.spec.ts | 2 +- 2 files changed, 587 insertions(+), 2 deletions(-) diff --git a/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.spec.ts b/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.spec.ts index 07cfc1db..03b96f9a 100644 --- a/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.spec.ts +++ b/packages/typescript-resolver-files/src/generateResolverFiles/addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented.spec.ts @@ -9,7 +9,7 @@ import type { ObjectTypeFile } from './types'; const createFilePath = (filePath: string): string => path.join('/path/', filePath); -describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented()', () => { +describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented() - mode: smart', () => { it('adds missing field resolvers if needed', () => { const project = new Project(); project.createSourceFile( @@ -175,6 +175,7 @@ describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented()', () = }, }; addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ + mode: 'smart', addedPropertyAssignmentNodes, sourceFile, resolverFile, @@ -362,6 +363,7 @@ describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented()', () = }, }; addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ + mode: 'smart', addedPropertyAssignmentNodes, sourceFile, resolverFile, @@ -545,6 +547,7 @@ describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented()', () = const addedPropertyAssignmentNodes: AddedPropertyAssignmentNodes = {}; addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ + mode: 'smart', addedPropertyAssignmentNodes, sourceFile, resolverFile, @@ -587,3 +590,585 @@ describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented()', () = `); }); }); + +describe('addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented() - mode: fast', () => { + it('adds missing field resolvers if needed', () => { + const project = new Project(); + project.createSourceFile( + createFilePath('user.mappers.ts'), + `export interface UserMapper { + id: number; + firstName: string; + lastName: string; + role: 'ADMIN' | 'USER'; + createdAt: Date; + }` + ); + project.createSourceFile( + createFilePath('types.generated.ts'), + ` + import { UserMapper } from './user.mappers'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { + [K in keyof T]: T[K]; + }; + export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; + }; + export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; + }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + DateTime: Date | string; + }; + + export type Query = { + __typename: 'Query'; + me?: Maybe; + }; + + export type User = { + __typename: 'User'; + accountGitHub?: Maybe; + accountGoogle?: Maybe; + createdAt: Scalars['DateTime']; + fullName: Scalars['String']; + id: Scalars['ID']; + role: UserRole; + }; + + export type UserRole = 'ADMIN' | 'USER'; + + export type ResolverTypeWrapper = Promise | T; + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = + | ResolverFn + | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: any + ) => Promise | TResult; + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + DateTime: ResolverTypeWrapper; + Query: ResolverTypeWrapper<{}>; + User: ResolverTypeWrapper; + String: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + UserRole: UserRole; + Boolean: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + DateTime: Scalars['DateTime']; + Query: {}; + User: UserMapper; + String: Scalars['String']; + ID: Scalars['ID']; + Boolean: Scalars['Boolean']; + }; + + export type UserResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + > = { + accountGitHub?: Resolver< + Maybe, + ParentType, + ContextType + >; + accountGoogle?: Resolver< + Maybe, + ParentType, + ContextType + >; + createdAt?: Resolver; + fullName?: Resolver; + id?: Resolver; + role?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + ` + ); + + const addedPropertyAssignmentNodes: AddedPropertyAssignmentNodes = {}; + const sourceFile = project.createSourceFile( + createFilePath('User.ts'), + `import type { UserResolvers } from './types.generated'; + export const User: UserResolvers = { + /* Implement logic here */ + };` + ); + const resolverFile: ObjectTypeFile = { + __filetype: 'objectType', + content: '', + mainImportIdentifier: 'User', + filesystem: { + type: 'filesystem', + contentUpdated: false, + }, + meta: { + moduleName: 'user', + relativePathFromBaseToModule: ['user'], + normalizedResolverName: { + base: 'User', + withModule: 'user.User', + }, + resolverTypeImportDeclaration: '', + variableStatement: '', + resolverType: { + baseImport: 'UserResolvers', + final: 'UserResolvers', + otherVariants: [], + }, + resolversToGenerate: { + id: { + resolverName: 'id', + resolverDeclaration: `({ id }) => id`, + }, + accountGitHub: { + resolverName: 'accountGitHub', + resolverDeclaration: `({ accountGitHub }) => accountGitHub`, + }, + accountGoogle: { + resolverName: 'accountGoogle', + resolverDeclaration: `({ accountGoogle }) => accountGoogle`, + }, + fullName: { + resolverName: 'fullName', + resolverDeclaration: `({ fullName }) => fullName`, + }, + }, + }, + }; + addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ + mode: 'fast', + addedPropertyAssignmentNodes, + sourceFile, + resolverFile, + }); + + // This is `/path/User.ts` file. We target it this way because the path on windows is different from linux + const file = Object.values(addedPropertyAssignmentNodes)[0]; + + const addedNode1 = file[4]; + expect(addedNode1.__toBeRemoved).toBe(false); + expect(addedNode1.node.getText()).toBe('id: ({ id }) => id'); + + const addedNode2 = file[5]; + expect(addedNode2.__toBeRemoved).toBe(false); + expect(addedNode2.node.getText()).toBe( + 'accountGitHub: ({ accountGitHub }) => accountGitHub' + ); + + const addedNode3 = file[6]; + expect(addedNode3.__toBeRemoved).toBe(false); + expect(addedNode3.node.getText()).toBe( + 'accountGoogle: ({ accountGoogle }) => accountGoogle' + ); + + const addedNode4 = file[7]; + expect(addedNode4.__toBeRemoved).toBe(false); + expect(addedNode4.node.getText()).toBe( + 'fullName: ({ fullName }) => fullName' + ); + + expect(sourceFile.getText()).toMatchInlineSnapshot(` + "import type { UserResolvers } from './types.generated'; + export const User: UserResolvers = { + /* Implement logic here */ + id: ({ id }) => id, + accountGitHub: ({ accountGitHub }) => accountGitHub, + accountGoogle: ({ accountGoogle }) => accountGoogle, + fullName: ({ fullName }) => fullName + };" + `); + }); + + it('adds does not add missing field resolvers if not needed i.e. resolversToGenerate is {}', () => { + const project = new Project(); + project.createSourceFile( + createFilePath('user.mappers.ts'), + `export interface UserMapper { + id: number; + fullName: string; + role: 'ADMIN' | 'USER'; + createdAt: Date; + }` + ); + project.createSourceFile( + createFilePath('types.generated.ts'), + ` + import { UserMapper } from './user.mappers'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { + [K in keyof T]: T[K]; + }; + export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; + }; + export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; + }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + DateTime: Date | string; + }; + + export type Query = { + __typename: 'Query'; + me?: Maybe; + }; + + export type User = { + __typename: 'User'; + accountGitHub?: Maybe; + accountGoogle?: Maybe; + createdAt: Scalars['DateTime']; + fullName: Scalars['String']; + id: Scalars['ID']; + role: UserRole; + }; + + export type UserRole = 'ADMIN' | 'USER'; + + export type ResolverTypeWrapper = Promise | T; + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = + | ResolverFn + | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: any + ) => Promise | TResult; + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + DateTime: ResolverTypeWrapper; + Query: ResolverTypeWrapper<{}>; + User: ResolverTypeWrapper; + String: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + UserRole: UserRole; + Boolean: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + DateTime: Scalars['DateTime']; + Query: {}; + User: UserMapper; + String: Scalars['String']; + ID: Scalars['ID']; + Boolean: Scalars['Boolean']; + }; + + export type UserResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + > = { + accountGitHub?: Resolver< + Maybe, + ParentType, + ContextType + >; + accountGoogle?: Resolver< + Maybe, + ParentType, + ContextType + >; + createdAt?: Resolver; + fullName?: Resolver; + id?: Resolver; + role?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + ` + ); + + const addedPropertyAssignmentNodes: AddedPropertyAssignmentNodes = {}; + const sourceFile = project.createSourceFile( + createFilePath('User.ts'), + `import type { UserResolvers } from './types.generated'; + export const User: UserResolvers = {};` + ); + const resolverFile: ObjectTypeFile = { + __filetype: 'objectType', + content: '', + mainImportIdentifier: 'User', + filesystem: { + type: 'filesystem', + contentUpdated: false, + }, + meta: { + moduleName: 'user', + relativePathFromBaseToModule: ['user'], + normalizedResolverName: { + base: 'User', + withModule: 'user.User', + }, + resolverTypeImportDeclaration: '', + variableStatement: '', + resolverType: { + baseImport: 'UserResolvers', + final: 'UserResolvers', + otherVariants: [], + }, + resolversToGenerate: {}, + }, + }; + addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ + mode: 'fast', + addedPropertyAssignmentNodes, + sourceFile, + resolverFile, + }); + + // This is `/path/User.ts` file. We target it this way because the path on windows is different from linux + const file = Object.values(addedPropertyAssignmentNodes)[0]; + + expect(file).toEqual({}); + expect(sourceFile.getText()).toMatchInlineSnapshot(` + "import type { UserResolvers } from './types.generated'; + export const User: UserResolvers = {};" + `); + }); + + it('adds missing field resolvers, if necessary, when Mapper is a Class', () => { + const project = new Project(); + project.createSourceFile( + createFilePath('user.mappers.ts'), + `export class UserMapper { + id: number; + firstName: string; + lastName: string; + role: 'ADMIN' | 'USER'; + createdAt: Date; + }` + ); + project.createSourceFile( + createFilePath('types.generated.ts'), + ` + import { UserMapper } from './user.mappers'; + export type Maybe = T | null; + export type InputMaybe = Maybe; + export type Exact = { + [K in keyof T]: T[K]; + }; + export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; + }; + export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; + }; + /** All built-in and custom scalars, mapped to their actual values */ + export type Scalars = { + ID: string; + String: string; + Boolean: boolean; + Int: number; + Float: number; + DateTime: Date | string; + }; + + export type Query = { + __typename: 'Query'; + me?: Maybe; + }; + + export type User = { + __typename: 'User'; + accountGitHub?: Maybe; + accountGoogle?: Maybe; + createdAt: Scalars['DateTime']; + fullName: Scalars['String']; + id: Scalars['ID']; + role: UserRole; + }; + + export type UserRole = 'ADMIN' | 'USER'; + + export type ResolverTypeWrapper = Promise | T; + + export type ResolverWithResolve = { + resolve: ResolverFn; + }; + export type Resolver = + | ResolverFn + | ResolverWithResolve; + + export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: any + ) => Promise | TResult; + + /** Mapping between all available schema types and the resolvers types */ + export type ResolversTypes = { + DateTime: ResolverTypeWrapper; + Query: ResolverTypeWrapper<{}>; + User: ResolverTypeWrapper; + String: ResolverTypeWrapper; + ID: ResolverTypeWrapper; + UserRole: UserRole; + Boolean: ResolverTypeWrapper; + }; + + /** Mapping between all available schema types and the resolvers parents */ + export type ResolversParentTypes = { + DateTime: Scalars['DateTime']; + Query: {}; + User: UserMapper; + String: Scalars['String']; + ID: Scalars['ID']; + Boolean: Scalars['Boolean']; + }; + + export type UserResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + > = { + accountGitHub?: Resolver< + Maybe, + ParentType, + ContextType + >; + accountGoogle?: Resolver< + Maybe, + ParentType, + ContextType + >; + createdAt?: Resolver; + fullName?: Resolver; + id?: Resolver; + role?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; + }; + ` + ); + + const sourceFile = project.createSourceFile( + createFilePath('User.ts'), + `import type { UserResolvers } from './types.generated'; + export const User: UserResolvers = { + /* Implement logic here */ + };` + ); + const resolverFile: Parameters< + typeof addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented + >[0]['resolverFile'] = { + __filetype: 'objectType', + content: '', + filesystem: { + type: 'filesystem', + contentUpdated: false, + }, + mainImportIdentifier: 'User', + meta: { + moduleName: 'user', + relativePathFromBaseToModule: ['user'], + normalizedResolverName: { + base: 'User', + withModule: 'user.User', + }, + resolverTypeImportDeclaration: '', + variableStatement: '', + resolverType: { + baseImport: 'UserResolvers', + final: 'UserResolvers', + otherVariants: [], + }, + resolversToGenerate: { + id: { + resolverName: 'id', + resolverDeclaration: `({ id }) => id`, + }, + accountGitHub: { + resolverName: 'accountGitHub', + resolverDeclaration: `({ accountGitHub }) => accountGitHub`, + }, + accountGoogle: { + resolverName: 'accountGoogle', + resolverDeclaration: `({ accountGoogle }) => accountGoogle`, + }, + fullName: { + resolverName: 'fullName', + resolverDeclaration: `({ fullName }) => fullName`, + }, + }, + }, + }; + + const addedPropertyAssignmentNodes: AddedPropertyAssignmentNodes = {}; + addObjectTypeResolversPropertyAssignmentNodesIfNotImplemented({ + mode: 'fast', + addedPropertyAssignmentNodes, + sourceFile, + resolverFile, + }); + + // This is `/path/User.ts` file. We target it this way because the path on windows is different from linux + const file = Object.values(addedPropertyAssignmentNodes)[0]; + + const addedNode1 = file[4]; + expect(addedNode1.__toBeRemoved).toBe(false); + expect(addedNode1.node.getText()).toBe('id: ({ id }) => id'); + + const addedNode2 = file[5]; + expect(addedNode2.__toBeRemoved).toBe(false); + expect(addedNode2.node.getText()).toBe( + 'accountGitHub: ({ accountGitHub }) => accountGitHub' + ); + + const addedNode3 = file[6]; + expect(addedNode3.__toBeRemoved).toBe(false); + expect(addedNode3.node.getText()).toBe( + 'accountGoogle: ({ accountGoogle }) => accountGoogle' + ); + + const addedNode4 = file[7]; + expect(addedNode4.__toBeRemoved).toBe(false); + expect(addedNode4.node.getText()).toBe( + 'fullName: ({ fullName }) => fullName' + ); + + expect(sourceFile.getText()).toMatchInlineSnapshot(` + "import type { UserResolvers } from './types.generated'; + export const User: UserResolvers = { + /* Implement logic here */ + id: ({ id }) => id, + accountGitHub: ({ accountGitHub }) => accountGitHub, + accountGoogle: ({ accountGoogle }) => accountGoogle, + fullName: ({ fullName }) => fullName + };" + `); + }); +}); diff --git a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts index f3ea604f..76e5da3e 100644 --- a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts +++ b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts @@ -283,7 +283,7 @@ describe('validatePresetConfig - general', () => { expect(() => validatePresetConfig({ fixObjectTypeResolvers: 'not-valid-for-sure' }) ).toThrow( - 'Validation - presetConfig.fixObjectTypeResolvers must be an object, "smart" or "disabled" (default is "smart")' + 'Validation - presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "smart")' ); }); From b24fb5e6f8acd52785b312d3bc4202a7d5ba2dff Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Sun, 12 Oct 2025 22:45:51 +1100 Subject: [PATCH 12/21] Improve ts-morph practices --- ...getGraphQLObjectTypeResolversToGenerate.ts | 27 +++++++------------ .../getNodePropertyMap.ts | 8 ++---- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index 6009ea4d..87e53b32 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -86,21 +86,19 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ if (schemaType && userDefinedSchemaObjectTypeMap[schemaType]) { resolverTypesMap[schemaType] = { node, - properties: getNodePropertyMap({ - tsMorphProject, - node, - }), + properties: getNodePropertyMap({ node }), }; } }; - typesSourceFile - .getDescendantsOfKind(SyntaxKind.TypeAliasDeclaration) - .forEach(populateSchemaTypeResolversPropertyMap); - - typesSourceFile - .getDescendantsOfKind(SyntaxKind.InterfaceDeclaration) - .forEach(populateSchemaTypeResolversPropertyMap); + typesSourceFile.getDescendants().forEach((node) => { + if ( + node.isKind(SyntaxKind.TypeAliasDeclaration) || + node.isKind(SyntaxKind.InterfaceDeclaration) + ) { + populateSchemaTypeResolversPropertyMap(node); + } + }); // 3. Find resolvers to generate and add reason const result: GraphQLObjectTypeResolversToGenerate = {}; @@ -116,7 +114,6 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ mapper, }); const mapperPropsMap = getNodePropertyMap({ - tsMorphProject, node: mapperOriginalDeclarationNode, }); @@ -186,10 +183,7 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ const schemaType = generatedSchemaTypeNameMap[identifierName]; if (schemaType && userDefinedSchemaObjectTypeMap[schemaType]) { - schemaResolversTypePropertyMap[schemaType] = getNodePropertyMap({ - tsMorphProject, - node, - }); + schemaResolversTypePropertyMap[schemaType] = getNodePropertyMap({ node }); } }; typesSourceFile @@ -214,7 +208,6 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ mapper, }); const typeMapperPropertyMap = getNodePropertyMap({ - tsMorphProject, node: originalDeclarationNode, }); diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts index fba42d30..73a9e2f0 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts @@ -18,17 +18,13 @@ export type NodePropertyMap = Record; */ export const getNodePropertyMap = ({ node, - tsMorphProject, }: { node: Node | undefined; - tsMorphProject: Project; }): NodePropertyMap => { if (!node) { return {}; } - const typeChecker = tsMorphProject.getTypeChecker(); - const properties = ((): NodePropertyMapValue[] => { if (node.isKind(SyntaxKind.ClassDeclaration)) { const result: NodePropertyMapValue[] = []; @@ -36,8 +32,8 @@ export const getNodePropertyMap = ({ return result; } - return typeChecker - .getTypeAtLocation(node) + return node + .getType() .getProperties() .map((prop) => { return { From 26177119c86677d8b1a78cc575e46be9fbc936b6 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 13 Oct 2025 00:39:01 +1100 Subject: [PATCH 13/21] Fix e2e test for fast mode to mimic real use case --- .../modules/resolvers.generated.ts | 3 +++ .../modules/test/resolvers/Query/user.ts | 8 ++++++ .../modules/test/schema.graphqls | 2 +- .../modules/types.generated.ts | 27 +++++++------------ 4 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Query/user.ts diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts index 581ab159..22e222c1 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/resolvers.generated.ts @@ -1,8 +1,11 @@ /* This file was automatically generated. DO NOT UPDATE MANUALLY. */ import type { Resolvers } from './types.generated'; +import { user as Query_user } from './test/resolvers/Query/user'; import { Book } from './test/resolvers/Book'; import { User } from './test/resolvers/User'; export const resolvers: Resolvers = { + Query: { user: Query_user }, + Book: Book, User: User, }; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Query/user.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Query/user.ts new file mode 100644 index 00000000..724ad2b2 --- /dev/null +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/resolvers/Query/user.ts @@ -0,0 +1,8 @@ +import type { QueryResolvers } from './../../../types.generated'; +export const user: NonNullable = async ( + _parent, + _arg, + _ctx +) => { + /* Implement Query.user resolver logic here */ +}; diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls index b46a0238..75a69bb3 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls @@ -1,4 +1,4 @@ -extend type Base { +extend type Query { user: User } diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts index 84c2d602..918589ab 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts @@ -20,7 +20,6 @@ export type Incremental = | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never; }; -export type Omit = Pick>; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: { input: string; output: string | number }; @@ -30,11 +29,6 @@ export type Scalars = { Float: { input: number; output: number }; }; -export type Base = { - __typename?: 'Base'; - user?: Maybe; -}; - export type Book = { __typename?: 'Book'; id: Scalars['ID']['output']; @@ -45,6 +39,7 @@ export type Book = { export type Query = { __typename?: 'Query'; + user?: Maybe; }; export type User = { @@ -188,9 +183,6 @@ export type DirectiveResolverFn< /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { - Base: ResolverTypeWrapper< - Omit & { user?: Maybe } - >; Book: ResolverTypeWrapper; ID: ResolverTypeWrapper; String: ResolverTypeWrapper; @@ -201,7 +193,6 @@ export type ResolversTypes = { /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = { - Base: Omit & { user?: Maybe }; Book: BookMapper; ID: Scalars['ID']['output']; String: Scalars['String']['output']; @@ -210,13 +201,6 @@ export type ResolversParentTypes = { Boolean: Scalars['Boolean']['output']; }; -export type BaseResolvers< - ContextType = any, - ParentType extends ResolversParentTypes['Base'] = ResolversParentTypes['Base'] -> = { - user?: Resolver, ParentType, ContextType>; -}; - export type BookResolvers< ContextType = any, ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book'] @@ -231,6 +215,13 @@ export type BookResolvers< >; }; +export type QueryResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] +> = { + user?: Resolver, ParentType, ContextType>; +}; + export type UserResolvers< ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] @@ -292,7 +283,7 @@ export type UserResolvers< }; export type Resolvers = { - Base?: BaseResolvers; Book?: BookResolvers; + Query?: QueryResolvers; User?: UserResolvers; }; From 6eeea39fe69dd9f3c9277effff2b2e37df863443 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 13 Oct 2025 00:39:30 +1100 Subject: [PATCH 14/21] DEBUG test with profiler --- ...getGraphQLObjectTypeResolversToGenerate.ts | 33 ++++++++++++------- .../typescript-resolver-files/src/preset.ts | 1 + 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index 87e53b32..9dfdef9c 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -13,19 +13,21 @@ import type { TypeMapperDetails, TypeMappersMap } from '../parseTypeMappers'; import { type NodePropertyMap, getNodePropertyMap } from './getNodePropertyMap'; import type { ParsedGraphQLSchemaMeta } from '../parseGraphQLSchema'; import type { GeneratedTypesFileMeta } from '../generateResolverFiles'; +import { Profiler } from '@graphql-codegen/plugin-helpers'; export type GraphQLObjectTypeResolversToGenerate = Record< string, Record >; -export const getGraphQLObjectTypeResolversToGenerate = ({ +export const getGraphQLObjectTypeResolversToGenerate = async ({ mode, tsMorphProject, typesSourceFile, typeMappersMap, userDefinedSchemaObjectTypeMap, generatedTypesFileMeta, + profiler, }: { mode: 'smart' | 'fast'; tsMorphProject: Project; @@ -33,7 +35,8 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ typeMappersMap: TypeMappersMap; userDefinedSchemaObjectTypeMap: ParsedGraphQLSchemaMeta['userDefinedSchemaTypeMap']['object']; generatedTypesFileMeta: GeneratedTypesFileMeta; -}): GraphQLObjectTypeResolversToGenerate => { + profiler: Profiler; +}): Promise => { const typeMappersEntries = Object.entries(typeMappersMap); if (typeMappersEntries.length === 0) { return {}; @@ -76,29 +79,37 @@ export const getGraphQLObjectTypeResolversToGenerate = ({ properties: NodePropertyMap; } > = {}; - const populateSchemaTypeResolversPropertyMap = ( + const populateSchemaTypeResolversPropertyMap = async ( node: TypeAliasDeclaration | InterfaceDeclaration - ): void => { + ): Promise => { const identifierName = node.getNameNode().getText(); // e.g. UserResolvers, BookResolvers const schemaType = generatedSchemaTypeNameMap[identifierName]; // schemaType examples: User, Book if (schemaType && userDefinedSchemaObjectTypeMap[schemaType]) { - resolverTypesMap[schemaType] = { - node, - properties: getNodePropertyMap({ node }), - }; + await profiler.run(async () => { + resolverTypesMap[schemaType] = { + node, + properties: getNodePropertyMap({ node }), + }; + }, `[test]: ${schemaType}`); } }; - typesSourceFile.getDescendants().forEach((node) => { + const syntaxList = typesSourceFile.getChildAtIndex(0); // Assumption: Root node of the types + if (!syntaxList) { + throw new Error( + `Root node of generated types file doesn't exist. This likely means the file is empty. This shouldn't happen.` + ); + } + for (const node of syntaxList.getChildren()) { if ( node.isKind(SyntaxKind.TypeAliasDeclaration) || node.isKind(SyntaxKind.InterfaceDeclaration) ) { - populateSchemaTypeResolversPropertyMap(node); + await populateSchemaTypeResolversPropertyMap(node); } - }); + } // 3. Find resolvers to generate and add reason const result: GraphQLObjectTypeResolversToGenerate = {}; diff --git a/packages/typescript-resolver-files/src/preset.ts b/packages/typescript-resolver-files/src/preset.ts index cba4ddd8..8444567b 100644 --- a/packages/typescript-resolver-files/src/preset.ts +++ b/packages/typescript-resolver-files/src/preset.ts @@ -175,6 +175,7 @@ export const preset: Types.OutputPreset = { mergedConfig.userDefinedSchemaTypeMap.object, typeMappersMap, generatedTypesFileMeta, + profiler, }), createProfilerRunName('getGraphQLObjectTypeResolversToGenerate') ); From 44bf3633b64da42be2900161e6d854179354c00f Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 13 Oct 2025 01:32:31 +1100 Subject: [PATCH 15/21] Update fast mode test setup --- .../modules/test/schema.graphqls | 4 ++++ .../modules/types.generated.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls index 75a69bb3..8623f1f3 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/test/schema.graphqls @@ -38,3 +38,7 @@ type Book { relatedBooks: [Book!]! nextBookInSeries: Book! } + +type NotMapped { + id: ID! +} diff --git a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts index 918589ab..7d79796d 100644 --- a/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts +++ b/packages/typescript-resolver-files-e2e/src/test-mappers-vs-schema-types-advanced/modules/types.generated.ts @@ -37,6 +37,11 @@ export type Book = { relatedBooks: Array; }; +export type NotMapped = { + __typename?: 'NotMapped'; + id: Scalars['ID']['output']; +}; + export type Query = { __typename?: 'Query'; user?: Maybe; @@ -186,6 +191,7 @@ export type ResolversTypes = { Book: ResolverTypeWrapper; ID: ResolverTypeWrapper; String: ResolverTypeWrapper; + NotMapped: ResolverTypeWrapper; Query: ResolverTypeWrapper>; User: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; @@ -196,6 +202,7 @@ export type ResolversParentTypes = { Book: BookMapper; ID: Scalars['ID']['output']; String: Scalars['String']['output']; + NotMapped: NotMapped; Query: Record; User: UserMapper; Boolean: Scalars['Boolean']['output']; @@ -215,6 +222,13 @@ export type BookResolvers< >; }; +export type NotMappedResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['NotMapped'] = ResolversParentTypes['NotMapped'] +> = { + id?: Resolver; +}; + export type QueryResolvers< ContextType = any, ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'] @@ -284,6 +298,7 @@ export type UserResolvers< export type Resolvers = { Book?: BookResolvers; + NotMapped?: NotMappedResolvers; Query?: QueryResolvers; User?: UserResolvers; }; From b229b1d699de7b73885bf142265073806446762f Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 13 Oct 2025 02:04:58 +1100 Subject: [PATCH 16/21] Update fast mode implementation --- ...getGraphQLObjectTypeResolversToGenerate.ts | 38 +++++++------------ .../getNodePropertyMap.ts | 1 - .../typescript-resolver-files/src/preset.ts | 1 - 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index 9dfdef9c..3daca848 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -27,7 +27,6 @@ export const getGraphQLObjectTypeResolversToGenerate = async ({ typeMappersMap, userDefinedSchemaObjectTypeMap, generatedTypesFileMeta, - profiler, }: { mode: 'smart' | 'fast'; tsMorphProject: Project; @@ -35,7 +34,6 @@ export const getGraphQLObjectTypeResolversToGenerate = async ({ typeMappersMap: TypeMappersMap; userDefinedSchemaObjectTypeMap: ParsedGraphQLSchemaMeta['userDefinedSchemaTypeMap']['object']; generatedTypesFileMeta: GeneratedTypesFileMeta; - profiler: Profiler; }): Promise => { const typeMappersEntries = Object.entries(typeMappersMap); if (typeMappersEntries.length === 0) { @@ -73,41 +71,31 @@ export const getGraphQLObjectTypeResolversToGenerate = async ({ if (mode === 'fast') { // 1. Get property map of all schema types const resolverTypesMap: Record< - string, - { - node: Node; - properties: NodePropertyMap; - } + string, // Schema type + { node: Node; properties: NodePropertyMap } > = {}; - const populateSchemaTypeResolversPropertyMap = async ( - node: TypeAliasDeclaration | InterfaceDeclaration - ): Promise => { - const identifierName = node.getNameNode().getText(); // e.g. UserResolvers, BookResolvers - - const schemaType = generatedSchemaTypeNameMap[identifierName]; // schemaType examples: User, Book - - if (schemaType && userDefinedSchemaObjectTypeMap[schemaType]) { - await profiler.run(async () => { - resolverTypesMap[schemaType] = { - node, - properties: getNodePropertyMap({ node }), - }; - }, `[test]: ${schemaType}`); - } - }; - const syntaxList = typesSourceFile.getChildAtIndex(0); // Assumption: Root node of the types + const syntaxList = typesSourceFile.getChildAtIndex(0); // Assumption: There is a root node in this file that contain all exports if (!syntaxList) { throw new Error( `Root node of generated types file doesn't exist. This likely means the file is empty. This shouldn't happen.` ); } + // Assumption: All resolvers exports are 1 level below root node i.e. we can use syntaxList.getChildren() for (const node of syntaxList.getChildren()) { if ( node.isKind(SyntaxKind.TypeAliasDeclaration) || node.isKind(SyntaxKind.InterfaceDeclaration) ) { - await populateSchemaTypeResolversPropertyMap(node); + const identifierName = node.getNameNode().getText(); // e.g. UserResolvers, BookResolvers + const schemaType = generatedSchemaTypeNameMap[identifierName]; // schemaType examples: User, Book + + if (schemaType && typeMappersMap[schemaType]) { + resolverTypesMap[schemaType] = { + node, + properties: getNodePropertyMap({ node }), + }; + } } } diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts index 73a9e2f0..77defea1 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.ts @@ -1,5 +1,4 @@ import { - type Project, type ClassDeclaration, type Node, type Type, diff --git a/packages/typescript-resolver-files/src/preset.ts b/packages/typescript-resolver-files/src/preset.ts index 8444567b..cba4ddd8 100644 --- a/packages/typescript-resolver-files/src/preset.ts +++ b/packages/typescript-resolver-files/src/preset.ts @@ -175,7 +175,6 @@ export const preset: Types.OutputPreset = { mergedConfig.userDefinedSchemaTypeMap.object, typeMappersMap, generatedTypesFileMeta, - profiler, }), createProfilerRunName('getGraphQLObjectTypeResolversToGenerate') ); From 4e6b116d37a566a1f5b2059ca3bb512df98cba2e Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Mon, 13 Oct 2025 23:16:10 +1100 Subject: [PATCH 17/21] Remove unused import --- .../getGraphQLObjectTypeResolversToGenerate.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index 3daca848..095f7dbe 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -13,7 +13,6 @@ import type { TypeMapperDetails, TypeMappersMap } from '../parseTypeMappers'; import { type NodePropertyMap, getNodePropertyMap } from './getNodePropertyMap'; import type { ParsedGraphQLSchemaMeta } from '../parseGraphQLSchema'; import type { GeneratedTypesFileMeta } from '../generateResolverFiles'; -import { Profiler } from '@graphql-codegen/plugin-helpers'; export type GraphQLObjectTypeResolversToGenerate = Record< string, From e02463613f7303c2a98b44f2eb210fd15f186274 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 14 Oct 2025 00:13:57 +1100 Subject: [PATCH 18/21] Fix tests --- .../getNodePropertyMap.spec.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.spec.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.spec.ts index adb37012..019ff378 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.spec.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getNodePropertyMap.spec.ts @@ -32,10 +32,7 @@ describe('getNodePropertyMap', () => { (node) => Node.isTypeAliasDeclaration(node) && node.getName() === 'User' ); - const nodePropertyMap = getNodePropertyMap({ - tsMorphProject: project, - node: userDeclarationNode, - }); + const nodePropertyMap = getNodePropertyMap({ node: userDeclarationNode }); expect(nodePropertyMap.__typename.name).toBe('__typename'); @@ -76,10 +73,7 @@ describe('getNodePropertyMap', () => { (node) => Node.isClassDeclaration(node) && node.getName() === 'User' ); - const nodePropertyMap = getNodePropertyMap({ - tsMorphProject: project, - node: userDeclarationNode, - }); + const nodePropertyMap = getNodePropertyMap({ node: userDeclarationNode }); expect(nodePropertyMap._id).toBe(undefined); expect(nodePropertyMap.id).toBe(undefined); @@ -110,10 +104,7 @@ describe('getNodePropertyMap', () => { descendant.getName() === 'UserMapper' ); - const nodePropertyMap = getNodePropertyMap({ - tsMorphProject: project, - node, - }); + const nodePropertyMap = getNodePropertyMap({ node }); expect(nodePropertyMap.id.name).toBe('id'); From a3e2c1599d461d16c0de602e072970ffe99dd2d6 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 14 Oct 2025 00:31:51 +1100 Subject: [PATCH 19/21] Make getGraphQLObjectTypeResolversToGenerate sync like previous --- .../getGraphQLObjectTypeResolversToGenerate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts index 095f7dbe..b22d5d4b 100644 --- a/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts +++ b/packages/typescript-resolver-files/src/getGraphQLObjectTypeResolversToGenerate/getGraphQLObjectTypeResolversToGenerate.ts @@ -19,7 +19,7 @@ export type GraphQLObjectTypeResolversToGenerate = Record< Record >; -export const getGraphQLObjectTypeResolversToGenerate = async ({ +export const getGraphQLObjectTypeResolversToGenerate = ({ mode, tsMorphProject, typesSourceFile, @@ -33,7 +33,7 @@ export const getGraphQLObjectTypeResolversToGenerate = async ({ typeMappersMap: TypeMappersMap; userDefinedSchemaObjectTypeMap: ParsedGraphQLSchemaMeta['userDefinedSchemaTypeMap']['object']; generatedTypesFileMeta: GeneratedTypesFileMeta; -}): Promise => { +}): GraphQLObjectTypeResolversToGenerate => { const typeMappersEntries = Object.entries(typeMappersMap); if (typeMappersEntries.length === 0) { return {}; From c8c39ce0921422ae96762404869a2e931c132010 Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 14 Oct 2025 21:55:45 +1100 Subject: [PATCH 20/21] Make fast default --- .../src/validatePresetConfig/validatePresetConfig.spec.ts | 6 +++--- .../src/validatePresetConfig/validatePresetConfig.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts index 76e5da3e..9d69fd88 100644 --- a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts +++ b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.spec.ts @@ -44,8 +44,8 @@ const defaultExpected: ParsedPresetConfig = { skipAddingFilesFromTsConfig: true, }, fixObjectTypeResolvers: { - object: 'smart', - enum: 'smart', + object: 'fast', + enum: 'fast', }, emitLegacyCommonJSImports: true, }; @@ -283,7 +283,7 @@ describe('validatePresetConfig - general', () => { expect(() => validatePresetConfig({ fixObjectTypeResolvers: 'not-valid-for-sure' }) ).toThrow( - 'Validation - presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "smart")' + 'Validation - presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "fast")' ); }); diff --git a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts index 383a7490..0ba72a9d 100644 --- a/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts +++ b/packages/typescript-resolver-files/src/validatePresetConfig/validatePresetConfig.ts @@ -144,7 +144,7 @@ export const validatePresetConfig = ({ externalResolvers = {}, typesPluginsConfig = {}, tsConfigFilePath = './tsconfig.json', - fixObjectTypeResolvers = 'smart', + fixObjectTypeResolvers = 'fast', emitLegacyCommonJSImports = true, }: RawPresetConfig): ParsedPresetConfig => { if (mode !== 'merged' && mode !== 'modules') { @@ -164,7 +164,7 @@ export const validatePresetConfig = ({ ) { throw new Error( fmt.error( - 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "smart")', + 'presetConfig.fixObjectTypeResolvers must be an object, "smart", "fast" or "disabled" (default is "fast")', 'Validation' ) ); From 109f785e623b72da49e7d8dfb3559e82c9b0de2f Mon Sep 17 00:00:00 2001 From: Eddy Nguyen Date: Tue, 14 Oct 2025 22:19:19 +1100 Subject: [PATCH 21/21] Update changeset --- .changeset/shaky-llamas-scream.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.changeset/shaky-llamas-scream.md b/.changeset/shaky-llamas-scream.md index d46358fd..364f2e4e 100644 --- a/.changeset/shaky-llamas-scream.md +++ b/.changeset/shaky-llamas-scream.md @@ -2,4 +2,6 @@ '@eddeee888/gcg-typescript-resolver-files': minor --- -Add fixObjectTypeResolvers 'fast' mode (experimental) +Add fixObjectTypeResolvers `fast` mode (experimental) + +This is now the default static analysis mode. If you see behaviour or runtime issues, please set `fixObjectTypeResolvers: smart` and file a report at