Comparing version 0.0.0-main-d90f90f3 to 0.0.0-main-d9b4f148
{ | ||
"name": "grats", | ||
"version": "0.0.0-main-d90f90f3", | ||
"main": "dist/index.js", | ||
"bin": "dist/cli.js", | ||
"types": "dist/index.d.ts", | ||
"version": "0.0.0-main-d9b4f148", | ||
"main": "dist/src/index.js", | ||
"bin": "dist/src/cli.js", | ||
"types": "dist/src/index.d.ts", | ||
"license": "MIT", | ||
"files": [ | ||
"dist/src" | ||
"dist", | ||
"!dist/src/tests" | ||
], | ||
"dependencies": { | ||
"@graphql-tools/utils": "^9.2.1", | ||
"commander": "^10.0.0", | ||
"graphql": "^16.6.0", | ||
"typescript": "^4.9.5" | ||
"typescript": "^5.0.2" | ||
}, | ||
"devDependencies": { | ||
"@graphql-tools/utils": "^9.2.1", | ||
"@types/node": "^18.14.6", | ||
"@types/semver": "^7.5.6", | ||
"@typescript-eslint/eslint-plugin": "^5.55.0", | ||
@@ -25,3 +27,5 @@ "@typescript-eslint/parser": "^5.55.0", | ||
"path-browserify": "^1.0.1", | ||
"prettier": "^2.8.7", | ||
"process": "^0.11.10", | ||
"semver": "^7.5.4", | ||
"ts-node": "^10.9.1" | ||
@@ -32,8 +36,34 @@ }, | ||
}, | ||
"packageManager": "pnpm@8.12.0", | ||
"engines": { | ||
"node": ">=16 <=21", | ||
"pnpm": "^8" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/captbaritone/grats/issues" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/captbaritone/grats.git" | ||
}, | ||
"author": { | ||
"name": "Jordan Eldredge", | ||
"email": "jordan@jordaneldredge.com", | ||
"url": "https://jordaneldredge.com" | ||
}, | ||
"keywords": [ | ||
"graphql", | ||
"typescript", | ||
"resolvers", | ||
"schema", | ||
"code-first", | ||
"implementation-first" | ||
], | ||
"scripts": { | ||
"test": "ts-node --esm src/tests/test.ts", | ||
"test": "ts-node src/tests/test.ts", | ||
"integration-tests": "node src/tests/integration.mjs", | ||
"build": "tsc", | ||
"lint": "eslint src/**/*.ts" | ||
"build": "rm -rf dist/ && tsc --build", | ||
"format": "prettier . --write", | ||
"lint": "eslint . && prettier . --check" | ||
} | ||
} |
531
README.md
@@ -1,5 +0,1 @@ | ||
# -=[ EXPERIMENTAL PRE ALPHA ]=- | ||
**This is currently a proof of concept. It won't yet work on any real projects.** | ||
# Grats: Implementation-First GraphQL for TypeScript | ||
@@ -9,44 +5,28 @@ | ||
Grats is a tool for statically infering GraphQL schema from your vanilla | ||
TypeScript code. | ||
_Beta Software: Grats is largely stable and being used in production in multiple places. If you encounter any issues, don't hesitate to let us know._ | ||
Just write your types and resolvers as regular TypeScript and annotate your | ||
types and fields with simple JSDoc tags. From there, Grats can extract your | ||
GraphQL schema automatically by statically analyzing your code and its types. No | ||
convoluted directive APIs to remember. No need to define your Schema at | ||
runtime with verbose builder APIs. | ||
**What if building a GraphQL server were as simple as just writing functions?** | ||
By making your TypeScript implementation the source of truth, you entirely | ||
remove the question of mismatches between your implementation and your GraphQL | ||
schema definition. Your implementation _is_ the schema definition! | ||
When you write your GraphQL server in TypeScript, your fields and resolvers | ||
are _already_ annotated with type information. _Grats leverages your existing | ||
type annotations to automatically extract an executable GraphQL schema from your | ||
generic TypeScript resolver code._ | ||
By making your TypeScript implementation the source of truth, you never have to | ||
worry about validating that your implementation matches your schema. Your | ||
implementation _is_ your schema! | ||
## Example | ||
Here's what it looks like to define a User type with a greeting field using Grats: | ||
```ts | ||
/** @gqlType */ | ||
export default class Query { | ||
class User { | ||
/** @gqlField */ | ||
me(): UserResolver { | ||
return new UserResolver(); | ||
} | ||
/** | ||
* @gqlField | ||
* @deprecated Please use `me` instead. | ||
*/ | ||
viewer(): UserResolver { | ||
return new UserResolver(); | ||
} | ||
} | ||
name: string; | ||
/** | ||
* A user in our kick-ass system! | ||
* @gqlType User | ||
*/ | ||
class UserResolver { | ||
/** @gqlField */ | ||
name: string = 'Alice'; | ||
/** @gqlField */ | ||
greeting(args: { salutation: string }): string { | ||
return `${args.salutation}, ${this.name}`; | ||
greet(args: { greeting: string }): string { | ||
return `${args.greeting}, ${this.name}`; | ||
} | ||
@@ -56,432 +36,13 @@ } | ||
Extracts the following GraphQL schema: | ||
After running `npx grats`, you'll find a `schema.ts` module that exports an executable schema, and a `schema.graphql` file contains your GraphQL schema definition: | ||
```graphql | ||
type Query { | ||
me: User | ||
viewer: User @deprecated(reason: "Please use `me` instead.") | ||
} | ||
"""A user in our kick-ass system!""" | ||
type User { | ||
name: String | ||
greeting(salutation: String!): String | ||
greet(greeting: String!): String | ||
} | ||
``` | ||
**Give it a try in the [online playground](https://capt.dev/grats-sandbox)!** | ||
That's just the beginning! To learn more, **Read the docs: https://grats.capt.dev/** | ||
## Quick Start | ||
For dev mode or small projects, Grats offers a runtime extraction mode. This is | ||
the easiest way to get started with Grats, although you may find that it causes | ||
a slow startup time. For larger projects, you probably want to use the build | ||
mode (documentation to come). | ||
```sh | ||
npm install express express-graphql grats | ||
``` | ||
**Ensure your project has a `tsconfig.json` file.** | ||
```ts | ||
import * as express from "express"; | ||
import { graphqlHTTP } from "express-graphql"; | ||
import { extractGratsSchemaAtRuntime } from "grats"; | ||
/** @gqlType */ | ||
class Query { | ||
/** @gqlField */ | ||
hello(): string { | ||
return "Hello world!"; | ||
} | ||
} | ||
const app = express(); | ||
// At runtime Grats will parse your TypeScript project (including this file!) and | ||
// extract the GraphQL schema. | ||
const schema = extractGratsSchemaAtRuntime({ | ||
emitSchemaFile: "./schema.graphql", | ||
}); | ||
app.use( | ||
"/graphql", | ||
graphqlHTTP({ schema, rootValue: new Query(), graphiql: true }), | ||
); | ||
app.listen(4000); | ||
console.log("Running a GraphQL API server at http://localhost:4000/graphql"); | ||
``` | ||
Try it out on [CodeSandbox](https://capt.dev/grats-sandbox)! | ||
## Configuration | ||
Grats has a few configuration options. They can be set in your project's | ||
`tsconfig.json` file: | ||
```json5 | ||
{ | ||
"grats": { | ||
// Should all fields be typed as nullable in accordance with GraphQL best | ||
// practices? | ||
// https://graphql.org/learn/best-practices/#nullability | ||
// | ||
// Individual fileds can declare themselves as nonnullable by adding the | ||
// docblock tag `@killsParentOnException`. | ||
"nullableByDefault": true, // Default: true | ||
// Should Grats error if it encounters a TypeScript type error? | ||
// Note that Grats will always error if it encounters a TypeScript syntax | ||
// error. | ||
"reportTypeScriptTypeErrors": false, // Default: false | ||
}, | ||
"compilerOptions": { | ||
// ... TypeScript config... | ||
} | ||
} | ||
``` | ||
## API Usage | ||
In order for Grats to extract GraphQL schema from your code, simply mark which | ||
TypeScript structures should be included in the schema by marking them with | ||
special JSDoc tags such as `/** @gqlType */` or `/** @gqlField */`. | ||
Any comment text preceding the JSDoc `@` tag will be used as that element's description. | ||
**Note that JSDocs must being with `/**` (two asterix).** However, they may be consolidated into a single line. | ||
The following JSDoc tags are supported: | ||
* [`@gqlType`](#gqltype) | ||
* [`@gqlInterface`](#gqlinterface) | ||
* [`@gqlField`](#gqlfield) | ||
* [`@gqlUnion`](#gqlunion) | ||
* [`@gqlScalar`](#gqlscalar) | ||
* [`@gqlEnum`](#gqlenum) | ||
* [`@gqlInput`](#gqlinput) | ||
Each tag maps directly to a concept in the GraphQL [Schema Definition | ||
Language](https://graphql.org/learn/schema/) (SDL). The documentation below aims | ||
to be complete, but our hope is that you feel empowered to just slap one of | ||
these docblock tags on the relevent TypeScript class/type/method/etc in your | ||
code, and let Grats' helpful error messages guide you. | ||
### @gqlType | ||
GraphQL types can be defined by placing a `@gqlType` docblock directly before a: | ||
* Class declaration | ||
* Interface declaration | ||
* Type alias of a literal type | ||
```ts | ||
/** | ||
* Here I can write a description of my type that will be included in the schema. | ||
* @gqlType <optional name of the type, if different from class name> | ||
*/ | ||
class MyClass { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
```ts | ||
/** @gqlType */ | ||
interface MyType { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
```ts | ||
/** @gqlType */ | ||
type MyType = { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
Note: If your type implements a GraphQL interface or is a member of a GraphQL | ||
union, Grats will remind you to add a `__typename: "MyType"` property to your | ||
class or interface. | ||
### @gqlInterface | ||
GraphQL interfaces can be defined by placing a `@gqlInterface` docblock directly before an: | ||
* Interface declaration | ||
```ts | ||
/** | ||
* A description of my interface. | ||
* @gqlInterface <optional name of the type, if different from class name> | ||
*/ | ||
interface MyClass { | ||
/** @gqlField */ | ||
someField: string; | ||
} | ||
``` | ||
All `@gqlType` types which implement the interface in TypeScript will | ||
automatically implement it in GraphQL as well. | ||
**Note**: Types declared using type literals `type MyType = { ... }` cannot yet | ||
implement interfaces. For now, you must use a class declarations for types which | ||
implement interfaces. | ||
### @gqlField | ||
You can define GraphQL fields by placing a `@gqlField` directly before a: | ||
* Method declaration | ||
* Method signature | ||
* Property declaration | ||
* Property signature | ||
* Function declaration (with named export) | ||
```ts | ||
/** | ||
* A description of some field. | ||
* @gqlField <optional name of the field, if different from property name> | ||
*/ | ||
someField: string; | ||
/** @gqlField */ | ||
myField(): string { | ||
return "Hello World"; | ||
} | ||
``` | ||
#### Field nullability | ||
**Note**: By default, Grats makes all fields nullable in keeping with [GraphQL | ||
*best practices](https://graphql.org/learn/best-practices/#nullability). This | ||
*behavior can be changed by setting the config option `nullableByDefault` to | ||
`false`. | ||
With `nullableByDefault` _enabled_, you may declare an individual field as | ||
nonnullable adding the docblock tag `@killsParentOnException`. This will cause | ||
the field to be typed as non-nullable, but _it comes at a price_. Should the | ||
resolver throw, the error will bubble up to the first nullable parent. If | ||
`@killsParentOnException` is used too liberally, small errors can take down huge | ||
portions of your query. | ||
Dissabling `nullableByDefault` is equivilent to marking all nonnullable fields | ||
with `@killsParentOnException`. | ||
```ts | ||
/** | ||
* @gqlField | ||
* @killsParentOnException | ||
*/ | ||
myField(): string { | ||
return this.someOtherMethod(); | ||
} | ||
``` | ||
#### Field arguments | ||
If you wish to define arguments for a field, define your argument types inline: | ||
```ts | ||
/** @gqlField */ | ||
myField(args: { greeting: string }): string { | ||
return `${args.greeting} World`; | ||
} | ||
``` | ||
Default values for arguments can be defined by using the `=` operator with destructuring: | ||
```ts | ||
/** @gqlField */ | ||
myField({ greeting = "Hello" }: { greeting: string }): string { | ||
return `${greeting} World`; | ||
} | ||
``` | ||
```ts | ||
/** @gqlField */ | ||
myField({ greeting = { salutation: "Sup" } }: { greeting: GreetingConfig }): string { | ||
return `${greeting.salutation} World`; | ||
} | ||
``` | ||
#### Field descriptions | ||
Arguments can be given descriptions by using the `/**` syntax: | ||
```ts | ||
/** @gqlField */ | ||
myField(args: { | ||
/** A description of the greeting argument */ | ||
greeting: string | ||
}): string { | ||
return `${args.greeting} World`; | ||
} | ||
``` | ||
#### Deprecated fields | ||
To mark a field as deprecated, use the `@deprecated` JSDoc tag: | ||
```ts | ||
/** | ||
* @gqlField | ||
* @deprecated Please use myNewField instead. | ||
*/ | ||
myOldField(): string { | ||
return "Hello World"; | ||
} | ||
``` | ||
#### Functional style fields | ||
Sometimes you want to add a computed field to a non-class type, or extend base | ||
types like `Query` or `Mutation` from another file. Both of these usecases are | ||
enabled by placing a `@gqlField` before an 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 | ||
/** @gqlField */ | ||
export function userById(_: Query, args: {id: string}): User { | ||
return DB.getUserById(args.id); | ||
} | ||
``` | ||
Extending Mutation: | ||
```ts | ||
/** @gqlField */ | ||
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. | ||
### @gqlUnion | ||
GraphQL unions can be defined by placing a `@gqlUnion` docblock directly before a: | ||
* Type alias of a union of object types | ||
```ts | ||
/** | ||
* A description of my union. | ||
* @gqlUnion <optional name of the union, if different from type name> | ||
*/ | ||
type MyUnion = User | Post; | ||
``` | ||
### @gqlScalar | ||
GraphQL custom sclars can be defined by placing a `@gqlScalar` docblock directly before a: | ||
* Type alias declaration | ||
```ts | ||
/** | ||
* A description of my custom scalar. | ||
* @gqlScalar <optional name of the scalar, if different from type name> | ||
*/ | ||
type MyCustomString = string; | ||
``` | ||
Note: For the built-in GraphQL scalars that don't have a corresponding TypeScript type, Grats ships with type aliases you can import. You may be promted to use one of these by Grat if you try to use `number` in a positon from which Grat needs to infer a GraphQL type. | ||
```ts | ||
import { Float, Int, ID } from "grats"; | ||
/** @gqlType */ | ||
class Math { | ||
id: ID; | ||
/** @gqlField */ | ||
round(args: {float: Float}): Int { | ||
return Math.round(args.float); | ||
} | ||
} | ||
``` | ||
### @gqlEnum | ||
GraphQL enums can be defined by placing a `@gqlEnum` docblock directly before a: | ||
* TypeScript enum declaration | ||
* Type alias of a union of string literals | ||
```ts | ||
/** | ||
* A description of my enum. | ||
* @gqlEnum <optional name of the enum, if different from type name> | ||
*/ | ||
enum MyEnum { | ||
/** A description of my variant */ | ||
OK = "OK", | ||
/** A description of my other variant */ | ||
ERROR = "ERROR" | ||
} | ||
``` | ||
Note that the values of the enum are used as the GraphQL enum values, and must | ||
be string literals. | ||
To mark a variants as deprecated, use the `@deprecated` JSDoc tag directly before it: | ||
```ts | ||
/** @gqlEnum */ | ||
enum MyEnum { | ||
OK = "OK" | ||
/** @deprecated Please use OK instead. */ | ||
OKAY = "OKAY" | ||
ERROR = "ERROR" | ||
} | ||
``` | ||
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 | ||
* You cannot mark enum values as deprecated | ||
This is due to the fact that TypeScript does not see JSDoc comments as | ||
"attaching" to string literal types. | ||
```ts | ||
/** @gqlEnum */ | ||
type MyEnum = "OK" | "ERROR"; | ||
``` | ||
### @gqlInput | ||
GraphQL input types can be defined by placing a `@gqlInput` docblock directly before a: | ||
* Type alias declaration | ||
```ts | ||
/** | ||
* Description of my input type | ||
* @gqlInput <optional name of the input, if different from type name> | ||
*/ | ||
type MyInput = { | ||
name: string; | ||
age: number; | ||
}; | ||
``` | ||
## Example | ||
See `examples/express-graphql/` in the repo root for a working example. Here we | ||
run the static analysis at startup time. Nice for development, but not ideal for | ||
production where you would want to cache the schema and write it to disk for | ||
other tools to see. | ||
## Contributing | ||
@@ -491,49 +52,11 @@ | ||
# FAQ | ||
## Why would I _not_ want to use Grats | ||
Because Grats relies on static analysis to infer types, it requires that your | ||
GraphQL fields use types that can be statically analyzed. This means that you | ||
can't use complex derived types in positions where Grats needs to be able to | ||
infer the type. For example, field arguments and return values. | ||
Currently, Grats does not have a great way to handle the case where you want to | ||
expose structures that are not owned by your codebase. For example, if you want | ||
to expose a field that returns a type from a third-party library, or a type that | ||
is generated by some other codegen tool. Today, your best option is to define a | ||
wrapper resolver class. | ||
## Why use comments and not decorators? | ||
Using decorators to signal that a class/method/etc should be included in the | ||
schema would have some advantages: | ||
* The syntax is well defined, so it: | ||
* Can be checked/documented by TypeScript types | ||
* Formatted with tools like Prettier | ||
* Would not require custom parsing/validaiton rules | ||
However, it also has some disadvantages: | ||
* The feature is technically "experimental" in TypeScript and may change in the future. | ||
* Decorators cannot be applied to types, so it would precude the ability to | ||
define GraphQL constructs using types (e.g. interfaces, unions, etc). | ||
* Decorators cannot be applied to parameters, so it would preclude the ability | ||
to define GraphQL constructs using parameters (e.g. field arguments). | ||
* Decorators are a runtime construct, which means they must be imported and give | ||
the impression that they might have some runtime behavior. This is not the | ||
case for Grats, which is purely a static analysis tool. | ||
Given these tradeoffs, we've decided to use comments instead of decorators. | ||
# Acknowledgements | ||
* [@mofeiZ](https://github.com/mofeiZ) and [@alunyov](https://github/alunyov) for their Relay hack-week project exploring a similar idea. | ||
* [@josephsavona](https://github.com/josephsavona) for input on the design of [Relay Resolvers](https://relay.dev/docs/guides/relay-resolvers/) which inspired this project. | ||
* [@bradzacher](https://github.com/bradzacher) for tips on how to handle TypeScript ASTs. | ||
* Everyone who worked on Meta's Hack GraphQL server, the developer experince of which inspired this project. | ||
* A number of other projects which seem to have explored similar ideas in the past: | ||
* [ts2gql](https://github.com/convoyinc/ts2gql) | ||
* [ts2graphql](https://github.com/cevek/ts2graphql) | ||
* [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) | ||
- [@mofeiZ](https://github.com/mofeiZ) and [@alunyov](https://github/alunyov) for their Relay hack-week project exploring a similar idea. | ||
- [@josephsavona](https://github.com/josephsavona) for input on the design of [Relay Resolvers](https://relay.dev/docs/guides/relay-resolvers/) which inspired this project. | ||
- [@bradzacher](https://github.com/bradzacher) for tips on how to handle TypeScript ASTs. | ||
- Everyone who worked on Meta's Hack GraphQL server, the developer experience of which inspired this project. | ||
- A number of other projects which seem to have explored similar ideas in the past: | ||
- [ts2gql](https://github.com/convoyinc/ts2gql) | ||
- [ts2graphql](https://github.com/cevek/ts2graphql) | ||
- [typegraphql-reflection-poc](https://github.com/MichalLytek/typegraphql-reflection-poc) |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
275035
3
71
5657
0
2
13
60
+ Addedtypescript@5.7.3(transitive)
- Removed@graphql-tools/utils@^9.2.1
- Removed@graphql-tools/utils@9.2.1(transitive)
- Removed@graphql-typed-document-node/core@3.2.0(transitive)
- Removedtslib@2.8.1(transitive)
- Removedtypescript@4.9.5(transitive)
Updatedtypescript@^5.0.2