Skip to content
This repository has been archived by the owner on Nov 30, 2023. It is now read-only.

orta/redwood-codegen-api-types

Repository files navigation

Now: https://github.com/sdl-codegen/sdl-codegen

The long term goals of this repo is to get it in redwood, over time I will start moving the tech stack for this repo to be more inline with the code used inside redwood core.

How to use this in a Redwood project

In your root, add the dependency:

yarn -W add -D @orta/redwood-codegen-api-types

Then to run the codegen locally:

yarn redwood-alt-api-codegen

There are two params you can optionally pass:

  • [1] The cwd of your Redwood project: (. default)
  • [2] The folder you want the types to get added to: (./api/src/lib/types default)

With the following extra possible args:

  • [--eslint-fix] - auto apply ESLint fixes to the new changed files
  • [--rm-default-api-types] - remove the default API type file at [app_root]/api/types/graphql.d.ts`

What does that do?

The codegen will generate a .d.ts file for each service in your Redwood project, and 2 shared .d.ts files which contains all of the types for the whole schema. The types are generated from the GraphQL SDL, and the Prisma schema combined, so each [service].d.ts will only contain code which is relevant to that file.

It is now pretty much feature complete, having taken the number of compiler errors in my app down from ~160 to 0.

Why

Redwood's type generation is pretty reasonable for most projects, but I've found after ~15 months of using Redwood when I turned on TypeScript's strict mode, I've never once achieved 0 compiler messages and performance for auto-complete/errors was not great inside the API files.

This... is a mixed bag, I'm reasonably sure that the runtime code is right but I was struggling to understand the compiler errors due to the extreme flexibility in the types generated by graphql-codegen and when I tried to solve some of the issues in userland it kept feeling like I was applying too much type-foo which would be harder for others to understand and maintain.

So, this project is what I have been referring to as 'relay style' types codegen, where each service in the Redwood project gets its own .d.ts file which is hyper specific - taking into account the resolvers defined, the GraphQL schema and the types from Prisma.

I like to think of it as taking all of the work which happens in the type system, from types like this:

export type AccountRelationResolvers<
  ContextType = RedwoodGraphQLContext,
  ParentType extends ResolversParentTypes["Account"] = ResolversParentTypes["Account"]
> = {
  createdAt?: RequiredResolverFn<
    ResolversTypes["DateTime"],
    ParentType,
    ContextType
  >;
  email?: RequiredResolverFn<ResolversTypes["String"], ParentType, ContextType>;
  users?: RequiredResolverFn<
    Array<ResolversTypes["User"]>,
    ParentType,
    ContextType
  >;
};

export type ResolversParentTypes = {
  Account: MergePrismaWithSdlTypes<
    PrismaAccount,
    MakeRelationsOptional<Account, AllMappedModels>,
    AllMappedModels
  >;
};

// ...

and instead manually do the work at runtime, with an understanding of how Redwood works to simply be 'fully resolved' smallest possible types:

import type { Account as PAccount, User as PUser } from "@prisma/client";

type AccountAsParent = PAccount & { users: () => Promise<PUser[]> };

export interface AccountTypeResolvers {
  /** SDL: users: [User!]! */
  users: (
    args: undefined,
    obj: {
      root: AccountAsParent;
      context: RedwoodGraphQLContext;
      info: GraphQLResolveInfo;
    }
  ) => PUser[] | Promise<PUser[]> | (() => Promise<PUser[]>);
}

Obviously this is considerably less flexible than before, but the goal is to be exactly what I need for my large Redwood project and then if folks are interested in the same problems, we can collab on making it more flexible.

I'm not really sure it makes sense for the Redwood team to think about upstreaming the changes, mainly because there's a bunch of codebases out there with all sorts of edge cases - and I don't have time to deal with other people's edge cases.


Clever features

  • Separate sets of SDL types for whether the type is used in an input or output position (e.g. SDL User in vs Prisma use User out)
  • Merges comments from Prisma file + GQL into .d.ts files
  • You can set a generic param on a type resolver which can extend the parent's type:
export const User: UserTypeResolvers<{ cachedAccount?: Account }> = {
  account: (args, { root }) => {
    if (root.cachedAccount) return root.cachedAccount;
    return db.account.findUnique({ where: { id: root.accountID } });
  },
};
  • In dev mode it can run eslint fixes over the files it generates, so it is formatted correctly
  • You can use an eslint rule to auto-hook up the imports for you
  • Resolvers show what their SDL source of truth is when you hover over them
  • It manually type narrow's for your async resolvers, so that you can trivially re-use them from outside the service file e.g. on tests. Params are also made optional on the types if they are not included in the resolver's fn.

You can see what it looks like when running on a small, but real, Redwood project here:

How to work in this repo

  • Install deno
  • Clone the repo
  • Run deno task dev to start the dev server
  • Run tests via deno task test

The dev server will re-run against the fixtures in tests/vendor, you can use git to work with the diff.

You can make a .env in the root, and the dev server will also run against these paths:

MAIN_APP_PATH="/home/orta/dev/app/"
MAIN_TYPES_DEPLOY="/home/orta/dev/redwood-codegen-api-types/ignored/"

This is useful if you want to test your code against a real Redwood project, while also being able to see how it changes with the vendored sdl + services.

Done

  • Generating a shared library of types for the whole schema (for referencing inside your resolvers)
  • Query / Mutation resolvers are correctly typed
  • Comments from Prisma file, and SDL are included in the generated types
  • Resolvers on specific models need to be added
  • Create an internal representation of a GQL type which adds an optional marker to resolvers defined in the file.
  • Unit test runner
  • Deno to npm module for ease of deploy

TODO

  • Watch mode
  • Create an 'unused resolvers' interface for auto-complete on the main type resolvers? Unsure, feels a bit messy but might make for a good DX for redwood newbies

Deployment

This is a Deno app which used 'DNT' to deploy to npm, so you can run deno task build:npm [version] to build a node compatible version of the app.

deno task build:npm 0.0.1
deno task deploy:npm