diff --git a/website/docs/03-resolvers/_category_.json b/website/docs/03-resolvers/_category_.json index ac08a506..3c4f1b71 100644 --- a/website/docs/03-resolvers/_category_.json +++ b/website/docs/03-resolvers/_category_.json @@ -1,4 +1,4 @@ { - "collapsible": false, - "collapsed": false + "collapsible": true, + "collapsed": true } diff --git a/website/docs/04-docblock-tags/_category_.json b/website/docs/04-docblock-tags/_category_.json index 258ab00c..79378e31 100644 --- a/website/docs/04-docblock-tags/_category_.json +++ b/website/docs/04-docblock-tags/_category_.json @@ -1,4 +1,4 @@ { - "collapsed": false, - "collapsible": false + "collapsed": true, + "collapsible": true } diff --git a/website/docs/05-guides/_category_.json b/website/docs/05-guides/_category_.json index a216c67e..ecb7f508 100644 --- a/website/docs/05-guides/_category_.json +++ b/website/docs/05-guides/_category_.json @@ -1,6 +1,6 @@ { "label": "Guides", - "collapsed": false, - "collapsible": false, + "collapsed": true, + "collapsible": true, "link": {} } diff --git a/website/docs/06-alternatives/01-pothos.mdx b/website/docs/06-alternatives/01-pothos.mdx new file mode 100644 index 00000000..621056cd --- /dev/null +++ b/website/docs/06-alternatives/01-pothos.mdx @@ -0,0 +1,39 @@ +import GratsCode from "@site/src/components/GratsCode"; +import Example from "!!raw-loader!./snippets/pothos.out"; + +# Pothos + +From https://pothos-graphql.dev/ + +```typescript +import { createYoga } from "graphql-yoga"; +import { createServer } from "node:http"; +import SchemaBuilder from "@pothos/core"; + +const builder = new SchemaBuilder({}); + +builder.queryType({ + fields: (t) => ({ + hello: t.string({ + args: { + name: t.arg.string(), + }, + resolve: (parent, { name }) => `hello, ${name || "World"}`, + }), + }), +}); + +const yoga = createYoga({ + schema: builder.toSchema(), +}); + +const server = createServer(yoga); + +server.listen(3000, () => { + console.log("Visit http://localhost:3000/graphql"); +}); +``` + +## Grats + + diff --git a/website/docs/06-alternatives/02-typegraphql.mdx b/website/docs/06-alternatives/02-typegraphql.mdx new file mode 100644 index 00000000..b51d869f --- /dev/null +++ b/website/docs/06-alternatives/02-typegraphql.mdx @@ -0,0 +1,36 @@ +import GratsCode from "@site/src/components/GratsCode"; +import Example from "!!raw-loader!./snippets/typegraphql.out"; + +# TypeGraphQL + +From https://typegraphql.com/ + +```typescript +@ObjectType() +export class Rate { + @Field((type) => Int) + value: number; +} + +@ObjectType() +class Recipe { + @Field() + title: string; + + @Field({ nullable: true }) + description?: string; + + @Field((type) => [Float]) + ratings: number[]; + + @Field((type) => Float, { nullable: true }) + get averageRating() { + const sum = this.ratings.reduce((a, b) => a + b, 0); + return sum / this.ratings.length; + } +} +``` + +## Grats + + diff --git a/website/docs/06-alternatives/03-nexus.mdx b/website/docs/06-alternatives/03-nexus.mdx new file mode 100644 index 00000000..f0054b29 --- /dev/null +++ b/website/docs/06-alternatives/03-nexus.mdx @@ -0,0 +1,23 @@ +import GratsCode from "@site/src/components/GratsCode"; +import Example from "!!raw-loader!./snippets/nexus.out"; + +# Nexus + +From https://nexusjs.org/docs/getting-started/tutorial/chapter-writing-your-first-schema + +```typescript +import { objectType } from "nexus"; +export const Post = objectType({ + name: "Post", // <- Name of your type + definition(t) { + t.int("id"); // <- Field named `id` of type `Int` + t.string("title"); // <- Field named `title` of type `String` + t.string("body"); // <- Field named `body` of type `String` + t.boolean("published"); // <- Field named `published` of type `Boolean` + }, +}); +``` + +## Grats + + diff --git a/website/docs/06-alternatives/04-graphql-js.mdx b/website/docs/06-alternatives/04-graphql-js.mdx new file mode 100644 index 00000000..6a31bd87 --- /dev/null +++ b/website/docs/06-alternatives/04-graphql-js.mdx @@ -0,0 +1,61 @@ +import GratsCode from "@site/src/components/GratsCode"; +import Example from "!!raw-loader!./snippets/graphql-js.out"; + +# GraphQL.js + +From https://graphql.org/graphql-js/constructing-types/ + +```typescript +var express = require("express"); +var { graphqlHTTP } = require("express-graphql"); +var graphql = require("graphql"); + +// Maps id to User object +var fakeDatabase = { + a: { id: "a", name: "alice" }, + b: { id: "b", name: "bob" }, +}; + +// Define the User type +var userType = new graphql.GraphQLObjectType({ + name: "User", + fields: { + id: { type: graphql.GraphQLString }, + name: { type: graphql.GraphQLString }, + }, +}); + +// Define the Query type +var queryType = new graphql.GraphQLObjectType({ + name: "Query", + fields: { + user: { + type: userType, + // `args` describes the arguments that the `user` query accepts + args: { + id: { type: graphql.GraphQLString }, + }, + resolve: (_, { id }) => { + return fakeDatabase[id]; + }, + }, + }, +}); + +var schema = new graphql.GraphQLSchema({ query: queryType }); + +var app = express(); +app.use( + "/graphql", + graphqlHTTP({ + schema: schema, + graphiql: true, + }), +); +app.listen(4000); +console.log("Running a GraphQL API server at localhost:4000/graphql"); +``` + +## Grats + + diff --git a/website/docs/06-alternatives/05-gqtx.mdx b/website/docs/06-alternatives/05-gqtx.mdx new file mode 100644 index 00000000..31595788 --- /dev/null +++ b/website/docs/06-alternatives/05-gqtx.mdx @@ -0,0 +1,81 @@ +import GratsCode from "@site/src/components/GratsCode"; +import Example from "!!raw-loader!./snippets/gqtx.out"; + +# Gqtx + +From https://github.com/sikanhe/gqtx + +```typescript +import { Gql, buildGraphQLSchema } from "gqtx"; + +enum Role { + Admin, + User, +} + +type User = { + id: string; + role: Role; + name: string; +}; + +const users: User[] = [ + { id: "1", role: Role.Admin, name: "Sikan" }, + { id: "2", role: Role.User, name: "Nicole" }, +]; + +// We can declare the app context type once, and it will +// be automatically inferred for all our resolvers +declare module "gqtx" { + interface GqlContext { + viewerId: number; + users: User[]; + } +} + +const RoleEnum = Gql.Enum({ + name: "Role", + description: "A user role", + values: [ + { name: "Admin", value: Role.Admin }, + { name: "User", value: Role.User }, + ], +}); + +const UserType = Gql.Object({ + name: "User", + description: "A User", + fields: () => [ + Gql.Field({ name: "id", type: Gql.NonNull(Gql.ID) }), + Gql.Field({ name: "role", type: Gql.NonNull(RoleEnum) }), + Gql.Field({ name: "name", type: Gql.NonNull(Gql.String) }), + ], +}); + +const Query = Gql.Query({ + fields: [ + Gql.Field({ + name: "userById", + type: UserType, + args: { + id: Gql.Arg({ type: Gql.NonNullInput(Gql.ID) }), + }, + resolve: (_, args, ctx) => { + // `args` is automatically inferred as { id: string } + // `ctx` (context) is also automatically inferred as { viewerId: number, users: User[] } + const user = ctx.users.find((u) => u.id === args.id); + // Also ensures we return an `User | null | undefined` type + return user; + }, + }), + ], +}); + +const schema = buildGraphQLSchema({ + query: Query, +}); +``` + +## Grats + + diff --git a/website/docs/06-alternatives/_category_.json b/website/docs/06-alternatives/_category_.json new file mode 100644 index 00000000..79378e31 --- /dev/null +++ b/website/docs/06-alternatives/_category_.json @@ -0,0 +1,4 @@ +{ + "collapsed": true, + "collapsible": true +} diff --git a/website/docs/06-alternatives/index.md b/website/docs/06-alternatives/index.md new file mode 100644 index 00000000..00d26465 --- /dev/null +++ b/website/docs/06-alternatives/index.md @@ -0,0 +1,11 @@ +# Alternatives + +In this section, we have taken "hello world" examples from the documentation of various other competing tools and reimplemented them with Grats. The hope is that this will give you a quick sense of how it would feel to use Grats as compared to other tools. + +## Comparisons + +- [Pothos](./01-pothos.mdx) +- [TypeGraphQL](./02-typegraphql.mdx) +- [Nexus](./03-nexus.mdx) +- [GraphQL.js](./04-graphql-js.mdx) +- [Gqtx](./05-gqtx.mdx) diff --git a/website/docs/06-alternatives/snippets/gqtx.grats.ts b/website/docs/06-alternatives/snippets/gqtx.grats.ts new file mode 100644 index 00000000..2bc93119 --- /dev/null +++ b/website/docs/06-alternatives/snippets/gqtx.grats.ts @@ -0,0 +1,45 @@ +import { ID } from "grats"; +import { getSchema } from "./schema"; // Generated by Grats + +/** + * A user role + * @gqlEnum + */ +enum Role { + Admin = "Admin", + User = "User", +} + +/** @gqlType */ +type User = { + /** @gqlField */ + id: ID; + /** @gqlField */ + role: Role; + /** @gqlField */ + name: string; +}; + +const users: User[] = [ + { id: "1", role: Role.Admin, name: "Sikan" }, + { id: "2", role: Role.User, name: "Nicole" }, +]; + +interface GqlContext { + viewerId: number; + users: User[]; +} + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function userById( + _: Query, + args: { id: string }, + ctx: GqlContext, +): User | null { + return users.find((u) => u.id === args.id) || null; +} + +const schema = getSchema(); diff --git a/website/docs/06-alternatives/snippets/gqtx.out b/website/docs/06-alternatives/snippets/gqtx.out new file mode 100644 index 00000000..9688ebea --- /dev/null +++ b/website/docs/06-alternatives/snippets/gqtx.out @@ -0,0 +1,122 @@ +import { ID } from "grats"; +import { getSchema } from "./schema"; // Generated by Grats + +/** + * A user role + * @gqlEnum + */ +enum Role { + Admin = "Admin", + User = "User", +} + +/** @gqlType */ +type User = { + /** @gqlField */ + id: ID; + /** @gqlField */ + role: Role; + /** @gqlField */ + name: string; +}; + +const users: User[] = [ + { id: "1", role: Role.Admin, name: "Sikan" }, + { id: "2", role: Role.User, name: "Nicole" }, +]; + +interface GqlContext { + viewerId: number; + users: User[]; +} + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function userById( + _: Query, + args: { id: string }, + ctx: GqlContext, +): User | null { + return users.find((u) => u.id === args.id) || null; +} + +const schema = getSchema(); + +=== SNIP === +"""A user role""" +enum Role { + Admin + User +} + +type Query { + userById(id: String!): User +} + +type User { + id: ID + name: String + role: Role +} +=== SNIP === +import { userById as queryUserByIdResolver } from "./gqtx.grats"; +import { GraphQLSchema, GraphQLObjectType, GraphQLID, GraphQLString, GraphQLEnumType, GraphQLNonNull } from "graphql"; +export function getSchema(): GraphQLSchema { + const RoleType: GraphQLEnumType = new GraphQLEnumType({ + description: "A user role", + name: "Role", + values: { + Admin: { + value: "Admin" + }, + User: { + value: "User" + } + } + }); + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + id: { + name: "id", + type: GraphQLID + }, + name: { + name: "name", + type: GraphQLString + }, + role: { + name: "role", + type: RoleType + } + }; + } + }); + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + userById: { + name: "userById", + type: UserType, + args: { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return queryUserByIdResolver(source, args, context); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [RoleType, QueryType, UserType] + }); +} diff --git a/website/docs/06-alternatives/snippets/graphql-js.grats.ts b/website/docs/06-alternatives/snippets/graphql-js.grats.ts new file mode 100644 index 00000000..df2a3848 --- /dev/null +++ b/website/docs/06-alternatives/snippets/graphql-js.grats.ts @@ -0,0 +1,36 @@ +import express from "express"; +import { graphqlHTTP } from "express-graphql"; +import { getSchema } from "./schema"; // Generated by Grats + +// Maps id to User object +var fakeDatabase = { + a: { id: "a", name: "alice" }, + b: { id: "b", name: "bob" }, +}; + +/** @gqlType */ +type User = { + /** @gqlField */ + id: string; + /** @gqlField */ + name: string; +}; + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function user(_: Query, args: { id: string }): User { + return fakeDatabase[args.id]; +} + +var app = express(); +app.use( + "/graphql", + graphqlHTTP({ + schema: getSchema(), + graphiql: true, + }), +); +app.listen(4000); +console.log("Running a GraphQL API server at localhost:4000/graphql"); diff --git a/website/docs/06-alternatives/snippets/graphql-js.out b/website/docs/06-alternatives/snippets/graphql-js.out new file mode 100644 index 00000000..7a2d522e --- /dev/null +++ b/website/docs/06-alternatives/snippets/graphql-js.out @@ -0,0 +1,90 @@ +import express from "express"; +import { graphqlHTTP } from "express-graphql"; +import { getSchema } from "./schema"; // Generated by Grats + +// Maps id to User object +var fakeDatabase = { + a: { id: "a", name: "alice" }, + b: { id: "b", name: "bob" }, +}; + +/** @gqlType */ +type User = { + /** @gqlField */ + id: string; + /** @gqlField */ + name: string; +}; + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function user(_: Query, args: { id: string }): User { + return fakeDatabase[args.id]; +} + +var app = express(); +app.use( + "/graphql", + graphqlHTTP({ + schema: getSchema(), + graphiql: true, + }), +); +app.listen(4000); +console.log("Running a GraphQL API server at localhost:4000/graphql"); + +=== SNIP === +type Query { + user(id: String!): User +} + +type User { + id: String + name: String +} +=== SNIP === +import { user as queryUserResolver } from "./graphql-js.grats"; +import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLNonNull } from "graphql"; +export function getSchema(): GraphQLSchema { + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + id: { + name: "id", + type: GraphQLString + }, + name: { + name: "name", + type: GraphQLString + } + }; + } + }); + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + user: { + name: "user", + type: UserType, + args: { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args) { + return queryUserResolver(source, args); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [QueryType, UserType] + }); +} diff --git a/website/docs/06-alternatives/snippets/nexus.grats.ts b/website/docs/06-alternatives/snippets/nexus.grats.ts new file mode 100644 index 00000000..4fdc9f44 --- /dev/null +++ b/website/docs/06-alternatives/snippets/nexus.grats.ts @@ -0,0 +1,13 @@ +import { Int } from "grats"; + +/** @gqlType */ +type Post = { + /** @gqlField */ + id: Int; // <- Field named `id` of type `Int` + /** @gqlField */ + title: string; // <- Field named `title` of type `String` + /** @gqlField */ + body: string; // <- Field named `body` of type `String` + /** @gqlField */ + published: boolean; // <- Field named `published` of type `Boolean` +}; diff --git a/website/docs/06-alternatives/snippets/nexus.out b/website/docs/06-alternatives/snippets/nexus.out new file mode 100644 index 00000000..87bf7a31 --- /dev/null +++ b/website/docs/06-alternatives/snippets/nexus.out @@ -0,0 +1,51 @@ +import { Int } from "grats"; + +/** @gqlType */ +type Post = { + /** @gqlField */ + id: Int; // <- Field named `id` of type `Int` + /** @gqlField */ + title: string; // <- Field named `title` of type `String` + /** @gqlField */ + body: string; // <- Field named `body` of type `String` + /** @gqlField */ + published: boolean; // <- Field named `published` of type `Boolean` +}; + +=== SNIP === +type Post { + body: String + id: Int + published: Boolean + title: String +} +=== SNIP === +import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLBoolean } from "graphql"; +export function getSchema(): GraphQLSchema { + const PostType: GraphQLObjectType = new GraphQLObjectType({ + name: "Post", + fields() { + return { + body: { + name: "body", + type: GraphQLString + }, + id: { + name: "id", + type: GraphQLInt + }, + published: { + name: "published", + type: GraphQLBoolean + }, + title: { + name: "title", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [PostType] + }); +} diff --git a/website/docs/06-alternatives/snippets/pothos.grats.ts b/website/docs/06-alternatives/snippets/pothos.grats.ts new file mode 100644 index 00000000..810d53d4 --- /dev/null +++ b/website/docs/06-alternatives/snippets/pothos.grats.ts @@ -0,0 +1,21 @@ +import { createYoga } from "graphql-yoga"; +import { createServer } from "node:http"; +import { getSchema } from "./schema"; // Generated by Grats + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function hello(_: Query, args: { name?: string | null }): string { + return `hello, ${args.name || "World"}`; +} + +const yoga = createYoga({ + schema: getSchema(), +}); + +const server = createServer(yoga); + +server.listen(3000, () => { + console.log("Visit http://localhost:3000/graphql"); +}); diff --git a/website/docs/06-alternatives/snippets/pothos.out b/website/docs/06-alternatives/snippets/pothos.out new file mode 100644 index 00000000..bd2cfc65 --- /dev/null +++ b/website/docs/06-alternatives/snippets/pothos.out @@ -0,0 +1,55 @@ +import { createYoga } from "graphql-yoga"; +import { createServer } from "node:http"; +import { getSchema } from "./schema"; // Generated by Grats + +/** @gqlType */ +type Query = unknown; + +/** @gqlField */ +export function hello(_: Query, args: { name?: string | null }): string { + return `hello, ${args.name || "World"}`; +} + +const yoga = createYoga({ + schema: getSchema(), +}); + +const server = createServer(yoga); + +server.listen(3000, () => { + console.log("Visit http://localhost:3000/graphql"); +}); + +=== SNIP === +type Query { + hello(name: String): String +} +=== SNIP === +import { hello as queryHelloResolver } from "./pothos.grats"; +import { GraphQLSchema, GraphQLObjectType, GraphQLString } from "graphql"; +export function getSchema(): GraphQLSchema { + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + hello: { + name: "hello", + type: GraphQLString, + args: { + name: { + name: "name", + type: GraphQLString + } + }, + resolve(source, args) { + return queryHelloResolver(source, args); + } + } + }; + } + }); + return new GraphQLSchema({ + query: QueryType, + types: [QueryType] + }); +} diff --git a/website/docs/06-alternatives/snippets/typegraphql.grats.ts b/website/docs/06-alternatives/snippets/typegraphql.grats.ts new file mode 100644 index 00000000..e4455a20 --- /dev/null +++ b/website/docs/06-alternatives/snippets/typegraphql.grats.ts @@ -0,0 +1,19 @@ +import { Float, Int } from "grats"; + +/** @gqlType */ +class Recipe { + /** @gqlField */ + title: string; + + /** @gqlField */ + description?: string; + + /** @gqlField */ + ratings: Float[]; + + /** @gqlField */ + get averageRating(): Float | null { + const sum = this.ratings.reduce((a, b) => a + b, 0); + return sum / this.ratings.length; + } +} diff --git a/website/docs/06-alternatives/snippets/typegraphql.out b/website/docs/06-alternatives/snippets/typegraphql.out new file mode 100644 index 00000000..5d56493b --- /dev/null +++ b/website/docs/06-alternatives/snippets/typegraphql.out @@ -0,0 +1,57 @@ +import { Float, Int } from "grats"; + +/** @gqlType */ +class Recipe { + /** @gqlField */ + title: string; + + /** @gqlField */ + description?: string; + + /** @gqlField */ + ratings: Float[]; + + /** @gqlField */ + get averageRating(): Float | null { + const sum = this.ratings.reduce((a, b) => a + b, 0); + return sum / this.ratings.length; + } +} + +=== SNIP === +type Recipe { + averageRating: Float + description: String + ratings: [Float!] + title: String +} +=== SNIP === +import { GraphQLSchema, GraphQLObjectType, GraphQLFloat, GraphQLString, GraphQLList, GraphQLNonNull } from "graphql"; +export function getSchema(): GraphQLSchema { + const RecipeType: GraphQLObjectType = new GraphQLObjectType({ + name: "Recipe", + fields() { + return { + averageRating: { + name: "averageRating", + type: GraphQLFloat + }, + description: { + name: "description", + type: GraphQLString + }, + ratings: { + name: "ratings", + type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)) + }, + title: { + name: "title", + type: GraphQLString + } + }; + } + }); + return new GraphQLSchema({ + types: [RecipeType] + }); +} diff --git a/website/docs/06-faq/_category_.json b/website/docs/06-faq/_category_.json index 23aeb108..0d8c2c1d 100644 --- a/website/docs/06-faq/_category_.json +++ b/website/docs/06-faq/_category_.json @@ -1,6 +1,6 @@ { "label": "FAQ", - "collapsed": false, - "collapsible": false, + "collapsed": true, + "collapsible": true, "link": {} } diff --git a/website/scripts/gratsCode.js b/website/scripts/gratsCode.js index fcd4f8a4..3db43fd9 100644 --- a/website/scripts/gratsCode.js +++ b/website/scripts/gratsCode.js @@ -19,7 +19,7 @@ function processFile(file) { const files = [file /*, `src/Types.ts`*/]; let options = { nullableByDefault: true, - reportTypeScriptTypeErrors: true, + reportTypeScriptTypeErrors: false, }; const parsedOptions = { options: {}, diff --git a/website/src/components/GratsCode.tsx b/website/src/components/GratsCode.tsx index 16d73ba0..70a90f12 100644 --- a/website/src/components/GratsCode.tsx +++ b/website/src/components/GratsCode.tsx @@ -69,7 +69,7 @@ function PlaygroundLink({ ts }) { doc, config: { nullableByDefault: true, - reportTypeScriptTypeErrors: true, + reportTypeScriptTypeErrors: false, }, view: { showGratsDirectives: false,