kamilkisiela / graphql-hive

GraphQL Hive is a schema registry and observability
https://the-guild.dev/graphql/hive
MIT License
392 stars 83 forks source link

Proposal: Migrate to use Server Preset for resolvers #4741

Open eddeee888 opened 3 weeks ago

eddeee888 commented 3 weeks ago

Current situation

Currently, we are using @graphql-codegen/graphql-modules-preset for type-safety in each module. Here's the general workflow:

  1. Create a new module (if required)
  2. Create/update module.graphql.ts
  3. Add a mapper to share/mappers.ts (if required)
    • Add a new entry to mappers in codegen.mts
  4. Create a resolver map in resolvers.ts and wire up *Module.Resolvers to get scoped type safety
  5. 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:

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:

'./packages/services/api/src': defineConfig({
  add: {
    './__generated__/types.next.ts': {
      content: "import type { StripeTypes } from '@hive/stripe-billing';",
    },
  },
  typeDefsFilePath: false,
  resolverMainFileMode: 'modules',
  resolverTypesPath: './__generated__/types.ts',
  scalarsOverrides: {
    DateTime: { type: 'string' },
    Date: { type: 'string' },
    SafeInt: { type: 'number' },
    ID: { type: 'string' },
  },
}

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:

  1. The migration MUST be gradual. Otherwise, it might cause a lot of conflicts with new PRs
  2. Existing resolver logic MUST NOT change during the migration i.e. we just move existing resolver logic into generated Server Preset files
  3. Existing codegen logic e.g. type names, mapper types, etc. COULD change with a good reason to minimise chances of breaking.
  4. MUST rely on existing tests to minimise chances of breaking, SHOULD add tests if needed

Migration steps

  1. Move mappers from shared/mappers.ts to each module's module.graphql.mappers.ts, rewire codegen.mts. This makes adopting Server Preset convention easier.
  2. 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
        },
      },
    }),
  3. 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
  4. Once the migration is done, remove graphql-modules-preset

Proof Of Concept

https://github.com/kamilkisiela/graphql-hive/pull/4740