sam-goodwin / eventual

Build scalable and durable micro-services with APIs, Messaging and Workflows
https://docs.eventual.ai
MIT License
174 stars 4 forks source link

Design: GraphQL Commands (WIP) #388

Open thantos opened 1 year ago

thantos commented 1 year ago

Goal: support graphQL field resolvers and subscriptions through commands.

Sub-Goals:

  1. Support interop with RPC and Rest
  2. Minimize duplication
  3. avoid leaking AWS specifics
  4. maintain type safety

Assumptions

Topics

schema loading/building

GraphQL requires a GraphQL schema, much like our API gateway impl requires a OpenAPI Spec.

This schema must accurately reflect the types, functions, and subscriptions that the service provides.

Schema First A common pattern is to define the schema in a .graphql file and load it into the service. This is the main approach provided by CDK for AppSync.

However, AppSync has some behaviors that can only be configured within the schema and are AppSync specific. We'd prefer to avoid leaking AppSync specific specifications (like how we don't include API Gateway fields in the Open API Spec). For example, subscriptions must have an associated decorator mapping it to one or more mutations.

Others problems with schema first include locality and composition. Most eventual service configuration are defined local to the problem and then get composed to the final result. This means that a command/workflow/event/etc is created by itself in whatever local file system a user chooses. There is not a single point that the entire set of commands/workflows/events need to be defined by the developer. Schema first would require the developer to return to a single source to create all of the graphQL schema. A lack of composition also means that a plugin could not provide a graphQL type/resolver.

Code First One solution to needing to inject special fields would be to generate the schema from what we know instead of requiring the schema to be written by the developer. We know some information like the types and fields covered by the commands, but we do not have the complete type information.

Code First - Zod We have some type information from zod types. Currently zod is optional for APIs and RPC, but graphQL would require accurate input and output data. The larger issues are: 1) I was not able to find a nice library for zod to graphql 2) this was likely because zod does not map to graphql nicely. GraphQL requires names for everything, which is not provided by zod and graphql has special types like ID that zod cannot express.

Proposal

I think the final approach here should be a hybrid one.

  1. Accept an optional graphQL schema
  2. support mutating the schema via intrinsic functions - solves for the locality and composition issue
  3. Use the graphql lexer/parser to update the given schema as needed.
  4. Future: Write a library or find one that can turn zod into graphql types
// optionally load an existing schema, else start from empty
// can only be called once
graphQL.fromFile(path);

// optional code first operations
// append types
graphQL.type`
   type Query {
      getItems: String
   }
`

// optionally set the schema object, otherwise the default
// can only be called once
graphQL.schema({
   query: "Query",
   mutation: "Mutation"
})

// add a field to an existing type (or create it)
graphQL.appendType("Query")`
    getItems(filter?: String): String
`

schema retrieval

Developers have access to the OpenAPI spec via an intrinsic function

ApiSpecification.generate(); // generate allows the spec to be filtered 

The same should be true for graphQL

GraphQLSchema.generate();

command configuration/field binding

Commands can support graphQL field/function binding within the same interface. Just need to provide the type and field to resolve.

command("myCommand", {
   method: "GET",
   path: "/items",
   input: {
      filter: z.string().optional()
   },
   output: z.string(),
   graphQL: { type: "Query", field: "getItems" },
   ({filter}) => {
         return "";
   }
})

At runtime, this will create an app sync data source and resolver (JS resolver?) that invokes the command lambda data source. We'll need to support AppSync as a possible input to the lambda OR create another lambda for the command for app async. We'll need to map headers and other values to the command.

schema validation

error handling

authentication and middleware

errors

graphql client

subscriptions definition

subscription filters

subscription invalidation

subscriptions client

subscriptions auth