Create a resolver map in resolvers.ts and wire up *Module.Resolvers to get scoped type safety
Whilst implementing the resolvers, make sure the mapper types and schema types match up, otherwise there could be runtime error
Step 3, 4 and 5 have a few DX issues:
In step 3: It's repetitive to manually create mapper to the shared mappers, then add the mapper to codegen.mts. The mappers option is quite big now and will continue to grow as we add more mappers.
In step 4: It's repetitive to create a new file for every module (arguably not too bad)
In step 5: We have to be careful when dealing with type differences between mappers and schema types to avoid runtime errors.
Improvements with Server Preset
Server Preset (@eddeee888/gcg-typescript-resolver-files) addresses steps 3, 4 and 5 by reducing the repetitive tasks and improving runtime safety with static analysis:
In step 3, when we create mappers in each module, it will be automatically detected and used i.e. we do not need to update codegen.mts. The codegen config look something like this for any number of modules:
Note that this requires moving mappers into modules where the schema types are declared. The drawback is we need to move mappers if we move schema types to another module. However, this scenario is fairly rare.
In step 4, Server Preset automatically generates important resolver files (with some customisation options) and put it in a resolver map automatically. This means we don't need to rely on @graphql-codegen/graphql-modules-preset to scope module resolvers.
Note that the way Server Preset generates resolver files will break things up to smaller pieces so we won't end up with overly large resolver files.
In step 5, Server Preset does static analysis and compare mapper types vs schema types and force users to write resolvers if a field doesn't exist or the field type between mapper and schema type don't match.
Other considerations
Server Preset doesn't currently support things like MiddlewareMap or any specific graphql-modules types, so if we need those, we can find other ways to integrate them with Server Preset. Note that I couldn't find obvious instances of these scenarios but please let me know so I can find ways to do them.
Migration plan
So, if we want to migrate to use Server Preset, how do we do it?
Here's the guiding principles of the migration:
The migration MUST be gradual. Otherwise, it might cause a lot of conflicts with new PRs
Existing resolver logic MUST NOT change during the migration i.e. we just move existing resolver logic into generated Server Preset files
Existing codegen logic e.g. type names, mapper types, etc. COULD change with a good reason to minimise chances of breaking.
MUST rely on existing tests to minimise chances of breaking, SHOULD add tests if needed
Migration steps
Move mappers from shared/mappers.ts to each module's module.graphql.mappers.ts, rewire codegen.mts. This makes adopting Server Preset convention easier.
Keep the existing codegen setup for graphql-modules-preset, create a new codegen target in codegen.mts for Server Preset but do not apply it to any modules:
// codegen.mts
'./packages/services/api/src': defineConfig({
add: {
'./__generated__/types.next.ts': {
content: "import type { StripeTypes } from '@hive/stripe-billing';",
},
},
typeDefsFilePath: false, // Each module is using `gql` to generate typeDefs, so this is `false` to avoid doing the same job
resolverMainFileMode: 'modules', // This generates a resolver map for each module
resolverTypesPath: './__generated__/types.next.ts', // This generates a types file for Server Preset resolvers to use
blacklistedModules: [/* ALL module names */], // This makes sure no resolvers are created by Server Preset until the migration
// scalarsOverrides applies the same config as `scalars`
scalarsOverrides: {
DateTime: { type: 'string' },
Date: { type: 'string' },
SafeInt: { type: 'number' },
ID: { type: 'string' },
},
typesPluginsConfig: {
immutableTypes: true,
contextType: 'GraphQLModules.ModuleContext',
enumValues: {
// ... existing enum values to have parity with existing code
},
mappers: {
// ... existing mappers to have parity with existing code, until we start the migration
},
},
}),
Gradually migrate one module at a time:
Remove module to migrate from blacklistedModules
Remove mappers in the module being migrated from codegen.mts as Server Preset should start using module.graphql.mappers.ts
Make sure static analysis happens correctly in generated resolver files
Wire up the resolver map in resolvers.generated.ts instead of the one in resolvers.ts
Once the migration is done, remove graphql-modules-preset
Current situation
Currently, we are using
@graphql-codegen/graphql-modules-preset
for type-safety in each module. Here's the general workflow:module.graphql.ts
share/mappers.ts
(if required)codegen.mts
resolvers.ts
and wire up*Module.Resolvers
to get scoped type safetyStep 3, 4 and 5 have a few DX issues:
codegen.mts
. Themappers
option is quite big now and will continue to grow as we add more mappers.Improvements with Server Preset
Server Preset (@eddeee888/gcg-typescript-resolver-files) addresses steps 3, 4 and 5 by reducing the repetitive tasks and improving runtime safety with static analysis:
In step 3, when we create mappers in each module, it will be automatically detected and used i.e. we do not need to update
codegen.mts
. The codegen config look something like this for any number of modules:Note that this requires moving mappers into modules where the schema types are declared. The drawback is we need to move mappers if we move schema types to another module. However, this scenario is fairly rare.
In step 4, Server Preset automatically generates important resolver files (with some customisation options) and put it in a resolver map automatically. This means we don't need to rely on
@graphql-codegen/graphql-modules-preset
to scope module resolvers. Note that the way Server Preset generates resolver files will break things up to smaller pieces so we won't end up with overly large resolver files.In step 5, Server Preset does static analysis and compare mapper types vs schema types and force users to write resolvers if a field doesn't exist or the field type between mapper and schema type don't match.
Other considerations
Server Preset doesn't currently support things like
MiddlewareMap
or any specificgraphql-modules
types, so if we need those, we can find other ways to integrate them with Server Preset. Note that I couldn't find obvious instances of these scenarios but please let me know so I can find ways to do them.Migration plan
So, if we want to migrate to use Server Preset, how do we do it?
Here's the guiding principles of the migration:
Migration steps
shared/mappers.ts
to each module'smodule.graphql.mappers.ts
, rewirecodegen.mts
. This makes adopting Server Preset convention easier.graphql-modules-preset
, create a new codegen target incodegen.mts
for Server Preset but do not apply it to any modules:blacklistedModules
codegen.mts
as Server Preset should start usingmodule.graphql.mappers.ts
resolvers.generated.ts
instead of the one inresolvers.ts
graphql-modules-preset
Proof Of Concept
https://github.com/kamilkisiela/graphql-hive/pull/4740