Skip to content

Commit

Permalink
Support functional resolvers via GQLExtendType
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Mar 24, 2023
1 parent 41f2e30 commit 7605a7e
Show file tree
Hide file tree
Showing 40 changed files with 1,077 additions and 106 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ The following JSDoc tags are supported:
* [`@GQLScalar`](#GQLscalar)
* [`@GQLEnum`](#GQLenum)
* [`@GQLInput`](#GQLinput)
* [`@GQLExtendType`](#GQLExtendType)


### @GQLType
Expand Down Expand Up @@ -284,7 +285,7 @@ enum MyEnum {
}
```

We also support defining enums using a union of string literals, howerver there
We also support defining enums using a union of string literals, however there
are some limitations to this approach:

* You cannot add descriptions to enum values
Expand Down Expand Up @@ -318,6 +319,49 @@ type MyInput = {
};
```

### @GQLExtendType

Sometimes you want to add a computed field to a non-class type, or extend base
type like `Query` or `Mutation` from another file. Both of these usecases are
enabled by placing a `@GQLExtendType` before a:

* Exported function declaration

In this case, the function should expect an instance of the base type as the
first argument, and an object representing the GraphQL field arguments as the
second argument. The function should return the value of the field.

Extending Query:

```ts
/**
* Description of my field
* @GQLExtendType <optional name of the field, if different from function name>
*/
export function userById(_: Query, args: {id: string}): User {
return DB.getUserById(args.id);
}
```

Extending Mutation:

```ts
/**
* Delete a user. GOODBYE!
* @GQLExtendType <optional name of the mutation, if different from function name>
*/
export function deleteUser(_: Mutation, args: {id: string}): boolean {
return DB.deleteUser(args.id);
}
```

Note that Grats will use the type of the first argument to determine which type
is being extended. So, as seen in the previous examples, even if you don't need
access to the instance you should still define a typed first argument.

You can think of `@GQLExtendType` as equivalent to the `extend type` syntax in
GraphQL's schema definition language.

## Example

See `example-server/` in the repo root for a working example. Here we run the static
Expand Down
33 changes: 21 additions & 12 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# TODO

## Alpha
- [ ] Rewrite pitch/description
- [ ] Decouple from file system
- [ ] Ensure `__typename`?
- [ ] More literals
- [ ] Int
Expand All @@ -13,32 +11,43 @@
- [ ] List
- [ ] Object
- [ ] Null
- [ ] Build a playground
- Code on left, GraphQL on right
- Could we actually evaluate the resolvers? Maybe in a worker?
- Could we hook up GraphiQL 2?
- Could we surface TS errors?
- [ ] Table of contents for docs
- [ ] Mutations and Query fields as functions
- [ ] Define types from type literals
- [ ] Extend interfaces

## Beta
- [ ] Classes as interfaces. Classes which extend this interface will implement it in GraphQL.
- [ ] Support generic directives
- [ ] How do we handle arguments?
- [ ] Same as defaults?
- [ ] Try converting the Star Wars example server
- [ ] Better error message for non GraphQL types.
- [ ] A name
- [ ] Split out docs into multipe files
- [ ] Extract error messages to a separate file
- [ ] Mutations and Query fields as functions
- [ ] Validate that we don't shadow builtins
- [ ] Parse directives from all docblocks and attach them to the schema
- [ ] This will help catch places where people are using directives like @deprecated in unsupported positions
- [ ] Add header to generated schema file indicating it was generated.
- [ ] Add option to print sorted schema.
- [ ] Better capture ranges form GraphQL errors

## Examples, Guides and Documentation
- [ ] Add a guide for using with Apollo Server
- [ ] Add a guide for using with Express-graphql
- [ ] Add a guide for OOP style
- [ ] Add a guide for functional style
- [ ] Comparison to Nexus
- [ ] Comparison to Pothos
- [ ] Comparison to TypeGraphQL
- [ ] Migration guide from Nexus
- [ ] Migration guide from Pothos
- [ ] Migration guide from TypeGraphQL
- [ ] Post about what it means to be "True" code-first

## Future
- [ ] Improve playground
- Could we actually evaluate the resolvers? Maybe in a worker?
- Could we hook up GraphiQL 2?
- Could we surface TS errors?
- [ ] Can we ensure the context and ast arguments of resolvers are correct?
- [ ] Can we use TypeScript's inference to infer types?
- [ ] For example, a method which returns a string, or a property that has a default value.
- [ ] Define resolvers?
Expand Down
1 change: 1 addition & 0 deletions docs/comparisons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# How Grats Compares to Other Tools
35 changes: 35 additions & 0 deletions docs/vs_nexus/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PrismaClient } from "@prisma/client";
import express from "express";
import { graphqlHTTP } from "express-graphql";

const prisma = new PrismaClient();

// FIXME: Not supported yet
/** @GQLType */
type User = {
/** @GQLField */
email: string;
/** @GQLField */
name?: string;
};

/** @GQLType */
class Query {
allUsers(): Promise<User[]> {
return prisma.user.findMany();
}
}

const schema = makeSchema({
types: [User, Query],
});

const app = express();
app.use(
"/graphql",
graphqlHTTP({
schema,
}),
);

app.listen(4000);
37 changes: 37 additions & 0 deletions docs/vs_nexus/before.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PrismaClient } from "@prisma/client";
import { queryType, objectType, makeSchema } from "@nexus/schema";
import express from "express";
import { graphqlHTTP } from "express-graphql";

const prisma = new PrismaClient();

const User = objectType({
name: "User",
definition(t) {
t.string("email");
t.string("name", { nullable: true });
},
});

const Query = queryType({
definition(t) {
t.list.field("allUsers", {
type: "User",
resolve: () => prisma.user.findMany(),
});
},
});

const schema = makeSchema({
types: [User, Query],
});

const app = express();
app.use(
"/graphql",
graphqlHTTP({
schema,
}),
);

app.listen(4000);
Empty file added docs/vs_nexus/vs_nexus.md
Empty file.
35 changes: 35 additions & 0 deletions docs/vs_typegraphql/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PrismaClient } from "@prisma/client";
import express from "express";
import { graphqlHTTP } from "express-graphql";

const prisma = new PrismaClient();

/** @GQLType */
export class User {
/** @GQLField */
email: string;

/** @GQLField */
name?: string | null;
}

/** @GQLType */
export class Query {
/** @GQLField */
async allUsers() {
return prisma.user.findMany();
}
}

const schema = buildSchemaSync({
resolvers: [PostResolver, UserResolver],
});

const app = express();
app.use(
"/graphql",
graphqlHTTP({
schema,
}),
);
app.listen(4000);
36 changes: 36 additions & 0 deletions docs/vs_typegraphql/before.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PrismaClient } from "@prisma/client";
import { ObjectType, Field, ID, buildSchemaSync } from "type-graphql";
import express from "express";
import { graphqlHTTP } from "express-graphql";

const prisma = new PrismaClient();

@ObjectType()
export class User {
@Field()
email: string;

@Field((type) => String, { nullable: true })
name?: string | null;
}

@Resolver(User)
export class UserResolver {
@Query((returns) => [User], { nullable: true })
async allUsers() {
return prisma.user.findMany();
}
}

const schema = buildSchemaSync({
resolvers: [PostResolver, UserResolver],
});

const app = express();
app.use(
"/graphql",
graphqlHTTP({
schema,
}),
);
app.listen(4000);
26 changes: 26 additions & 0 deletions docs/vs_typegraphql/vs_typegraphql.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## TypeGraphQL + Prisma

```diff
2d1
< import { ObjectType, Field, ID, buildSchemaSync } from "type-graphql";
8c7
< @ObjectType()
---
> /** @GQLType */
10c9
< @Field()
---
> /** @GQLField */
13c12
< @Field((type) => String, { nullable: true })
---
> /** @GQLField */
17,19c16,18
< @Resolver(User)
< export class UserResolver {
< @Query((returns) => [User], { nullable: true })
---
> /** @GQLType */
> export class Query {
> /** @GQLField */
```
6 changes: 6 additions & 0 deletions example-server/models/User.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Query from "../Query";
import Group from "./Group";

/** @GQLType User */
Expand All @@ -11,3 +12,8 @@ export default class UserResolver {
return [new Group()];
}
}

/** @GQLExtendType */
export function allUsers(_: Query): UserResolver[] {
return [new UserResolver(), new UserResolver()];
}
17 changes: 10 additions & 7 deletions example-server/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ schema {
query: Query
}

directive @renameField(name: String!) on FIELD_DEFINITION
directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION

directive @methodName(name: String!) on FIELD_DEFINITION

type Group {
description: String
members: [User!]
name: String
description: String!
members: [User!]!
name: String!
}

type Query {
me: User
allUsers: [User!]! @exported(filename: "/Users/captbaritone/projects/grats/example-server/models/User.ts", functionName: "allUsers")
me: User!
}

type User {
groups: [Group!]
name: String
groups: [Group!]!
name: String!
}

0 comments on commit 7605a7e

Please sign in to comment.