Git Product home page Git Product logo

grats's People

Contributors

captbaritone avatar cpojer avatar leoasis avatar mandx avatar mrbrownt avatar orta avatar rbalicki2 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

grats's Issues

Current approach to interfaces is unsafe

When we mix:

  1. The ability to define interfaces using TS interfaces
  2. The ability to define types that implement interfaces using TS type
  3. The ability to define fields on interfaces using free functions that accept an instance of the TS interface

The result is unsafe. This is because TS does not have any way to validate that a type implements and interface. Therefore, the properties/methods we expect to find on the interface may not exist on the type, resulting in a runtime error.

/** @gqlInterface SomeInterface */
interface SomeInterface {
  /** @gqlField */
  some_field: string;
}

/**
 * @gqlType
 * @gqlImplements SomeInterface
 */
type SomeType = {
  __typename: "SomeType";
  /** @gqlField */
  name: string;
};

/**
 * @gqlField
 */
export function some_field(someType: SomeType): string {
  return someType.name;
}

/**
 * @gqlField
 */
export function some_other_field(parent: SomeInterface): string {
  // This could error at runtime since it might be called with an instance of `SomeType`
  return parent.some_field.toUpperCase();
}

Playground

Refactor: Update Extractor to be syntax only

Right now Extractor leverages the type context in ways that mutate it to track observed values. This makes the data tracking less explicit and also prevents an incremental future where individual file parses can be reused.

Instead, we should have Extractor return an artifact that is based purely on syntactic analysis and perform the mutation and definition lookups for a second phase.

So, ideal outcomes:

  • No type context beyond the current file is observed
  • No mutations to external structures (TypeContext)
  • Returns a concrete structure (ideally serializable)

Vertical scroll is broken in playground

If the content of the playground editor is longer than the page, scroll does not work correctly and the text overflows the footer instead of pushing the footer down.

Screenshot 2023-10-30 at 7 05 31 PM

`@gqlType` apparently wants to use the whole dockblock as the type's name

/**
 * @gqlType
 *
 * This is a note for myself
 */
export type Query = unknown;

/** @gqlField */
export function queryField(_: Query): string {
  return ''
}
Dumps this error
# ERROR MESSAGE
# =============

# Grats playground bug encountered. Please report this error:
# 
#  l@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2504646
# o@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2550813
# q@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2554958
# z@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2597543
# y@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2592981
# S@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2599039
# 8284/ue/r<@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:41216
# ue@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:41897
# 8284/qe/</o<@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:43569
# 8284/qe/<@https://grats.capt.dev/assets/js/8926b418.0a75dbcb.js:1:43763
# 7781/run/<@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2227204
# run@https://grats.capt.dev/assets/js/7492.5eb44cb1.js:28:2227180
# 

Playground link

The error dump doesn't give us much to work with, however, if we use this code:

/**
 * @gqlType
 *
 * ThisIsANoteForMyself
 */
export type Query = unknown;

/** @gqlField */
export function queryField(_: Query): string {
  return ''
}

We get this as the resulting schema:

type ThisIsANoteForMyself {
  queryField: String
}

Maybe @gqlType needs to take only until the next newline to extract the type's new name?

Support defining interfaces by extending classes

I had originally opted not to support defining interfaces via classes because of the mismatch between TypeScript having single inheritance and GraphQL having multiple inheritance. You can't express:

type User implements Person, Node {
  # ...
}

using only class inheritance with TypeScript. That said, people do have inheritance setup in their TypeScript code bases which make sense as interfaces as they expose that same code in GraphQL. Following this design principle, we ought to support this for the cases where users have such code.

There will be some challenges:

  1. How do we figure out from TypeScript which classes extend which other classes. Note that we need to include transitive cases. This may impact our incremental compilation strategy.
  2. We need to figure out how overrides work and ensure their ordering is applied correctly. If the concrete class and extended class both implement a field, which one wins? With multiple inheritance this gets tricky.

Remove dependency on graphql-tools

In order to "move fast" during development we added a dependency on graphql-tools/utils. Our use of them is pretty minimal, so in keeping with our Design Principles we should try to extract just the bits we need with our own minimal implementations.

Idea for discussion: Introduce `@gqlSchema.query`, `@gqlSchema.mutation` and `@gqlSchema.subscription`

These would tag a single object type as the type used in the corresponding schema declaration in the resulting SDL. So, something like this:

/**
 * @gqlType
 * @gqlSchema.query
 */
export class MyOddlyNamedQueryType {
  /** @gqlField */
  me(): string {
    return new 'yo';
  }
}

Would generate:

type MyOddlyNamedQueryType {
  me: String
}

schema {
  query: MyOddlyNamedQueryType
}

Same for @gqlSchema.mutation and @gqlSchema.subscription. Obviously, at build/introspection time, a check should run that asserts that each of these @gqlSchema.* appear only once. Defaults would still be used; so, there is a @gqlSchema.query on a type, but there's no @gqlSchema.mutation anywhere, it is assumed that the schema's mutation key would be whatever object type is named Mutation.

README for the playground is incomplete

As of right now, the playground is broken; any code on the left side yields a stack trace on the left side
# ERROR MESSAGE
# =============

# Grats playground bug encountered. Please report this error:
# 
#  Is@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4259459
# Rs@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4260178
# Js@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4260359
# vm@https://grats.capt.dev/assets/js/492.e7db54da.js:23:4343317
# ue@https://grats.capt.dev/assets/js/492.e7db54da.js:28:42020
# 1273/src/compiler/parser.ts/e.parseSourceFile/f<@https://grats.capt.dev/assets/js/492.e7db54da.js:28:41534
# 1273/src/compiler/parser.ts/e.parseSourceFile@https://grats.capt.dev/assets/js/492.e7db54da.js:28:41790
# wF@https://grats.capt.dev/assets/js/492.e7db54da.js:28:23938
# getSourceFile@https://grats.capt.dev/assets/js/492.e7db54da.js:2:5629
# 4182/e.createProgram/Nt/o<@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2716133
# Nt@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2717664
# 4182/e.createProgram/wt/<@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2714689
# Et@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2714080
# wt@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2714666
# St@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2711583
# 4182/e.createProgram/<@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2681888
# 4182/e.forEach@https://grats.capt.dev/assets/js/492.e7db54da.js:23:381412
# 4182/e.createProgram@https://grats.capt.dev/assets/js/492.e7db54da.js:23:2681857
# 8284/ue/i<@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:38188
# ue@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:39552
# 8284/Oe/</o<@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:41185
# 8284/Oe/<@https://grats.capt.dev/assets/js/8926b418.1f73541a.js:1:41379
# 7781/run/<@https://grats.capt.dev/assets/js/492.e7db54da.js:28:2227204
# run@https://grats.capt.dev/assets/js/492.e7db54da.js:28:2227180

I attempted to fix it, but cloning the repo, installing and running with yarn yields several import errors (which I tried to hack around) and then some more hook-related errors...

image
image
image

Confusing lack of any output when putting docblock below decorators

@ObjectType({
  description: "A person who has composed opera.",
})
/** @gqlType */
export default class Composer  {
  @Field((type) => String)
  /** @gqlField */
  url(): string {
    return `/composer/${this._composer.id}`;
  }
}

Silently extracts zero things. Moving the docblocks above the decorators works, but we should error in this case if possible to show you where to find them.

Support defining GraphQL types as TypeScript interfaces

We currently support extracting GraphQL type definitions from TypeScript class definitions and GraphQL interface definitions from TypeScript interface definitions. But it is reasonable that someone might want to expose an interface in their TypeScript code as a GraphQL type.

/** GQLType */
interface MyType {
  /** GQLField */
  myField: string
}

Should generate:

type MyType {
  myField: String
}

Pointers

  • Add a new case in Extractor.extractType to handle interface definitions
  • Copy the implementation of Extractor.typeClassDeclaration into a new method named typeInterfaceDeclaration, and make the necessary changes.
  • Create a new fixture directory that's a copy of fixtures/type_definition and its contents and call it fixtures/type_definition_from_interface or similar. Update all the tests to use interfaces.
  • Add documentation to the readme file indicating that interfaces can be used, and give an example.

See CONTRIBUTING.md for project level guidance.

Force optional args to accept null

If a field arg is typed in the DSL as optional, a request that passes an explicit null, is considered valid. This means graphql-js will pass through a user's explicit null. This, in turn, means that if Grats is going to generate an optional argument, the user's field resolver must be capable of accepting null.

Today it's possible to define a function which is not expecting in its typescript types, but could get passed a null at runtime:

/** @gqlType */
export default class Query {
  /** @gqlField */
  someField(args: {greeting: string | undefined}): string {
    if(args.greeting === undefined) {
      return "Hello!"
    }
    return args.greeting; // OOPS! Might return `null` here, which is not a `string`.
  }
}
type Query {
  someField(greeting: String): String
}

Playground

I think the best solution here is for Grats to error in this case and force you to add | null to the argument's type.

CLI not using args/options

I read this: https://grats.capt.dev/docs/usage/cli

  • grats -V doesn't show the version.
  • grats --output=./schema.graphql does not do anything.

grats cli issue

My TsConfig:

{
  "compilerOptions": {
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "module": "commonjs",                                /* Specify what module code is generated. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    "strict": true,                                      /* Enable all strict type-checking options. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

Leveraging directives in the schema is noisy

Currently Grats uses schema directives to encode metadata that it extracted from the code which it will is necessary for the correct execution of user's resolvers. For example, if you define a field on some type using a free function, Grats needs to tell the executor where that function lives.

/** @gqlField */
export function allUsers(_: Query): UserResolver[] {
  return [new UserResolver(), new UserResolver()];
}

Extracts:

type Query {
  allUsers: [User!]! @exported(filename: "../examples/express-graphql/dist/models/User.js", functionName: "allUsers")
  # ...
}

Since by default graphql-js looks for all fields as methods/properties on the parent object, the same issue occurs if you want to assign a field a name that differs from the property/method name and so Grats also has a @methodName directive.

Today this is achieved by the extraction portion of Relay adding server directives to each field that has additional data like a method name that differs, or a specific free function that should be called. Grats then "applies" these directives by wrapping the default resolver function for each field before returning the GraphQLSchema object.

This has a certain elegance because the extraction portion just derives a single, spec compliant SDL text, and starting up can be done by simply ingesting that single file.

However, the downside is that the schema becomes pretty noisy for human inspection.

I can see this tradoeff getting worse as/if we explore other features, like being able to accept positional arguments rather than requiring all args to be bundled into a single object.

Options

Here are the options I see right now, but hopefully others exist as well:

  1. Leave it the way it is. Like I said, there's an elegance to it, and maybe the visual noise is not such a big deal
  2. Emit a companion Grats metadata file alongside the schema. This adds complexity, but leaves the schema clean.
  3. Emit two schemas. One that includes Grats metadata, and one that's intended for human inspection or use by other tools

GraphQL location data seems a bit off.

See the diff in 7f2bada

src/tests/fixtures/field_values/StringFieldKillsParentOnExceptionWithoutNullableByDefaultEnables.invalid.ts.expected changes the highlight, but it should be the same range converted from ts to GraphQL and then back to ts for reporting. One (or more?) of those transforms must be wrong.

Can't use `async` or `Promise` in the playground: it errors complaining about an older `lib` target

As of 2023-10-30, when entering this code in the playground:

/** @gqlType */
class Query {
  /**
   * @gqlField
   */
  async test(): Promise<string> {
    return '';
  }
}

We get an error about Promise not being defined:

# ERROR MESSAGE
# =============

# index.ts:6:17 - error TS2705: An async function or method in ES5/ES3 requires the 'Promise' constructor.  Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your '--lib' option.
# 
# 6   async test(): Promise<string> {
#                   ~~~~~~~~~~~~~~~
# 

I thought this was going to be an easy thing to fix, except, it looks like the playground's TS config already targets a recent environment (ES2021)?

    createDefaultMapFromCDN(
      { target: ts.ScriptTarget.ES2021, lib: ["es2021"] },
      ts.version,
      shouldCache,
      ts,
      lzstring,
    )

However, I also found this commented line:

  const compilerOpts = {
    allowJs: true,
    baseUrl: "./",
    paths: { grats: [GRATS_PATH] },
    // lib: ["es2021"],
  };
  const host = createVirtualCompilerHost(system, compilerOpts, ts);

I'm curious what is the Chesterton's Fence reason for having lib: ["es2021"] in one component but not in the other?

TS Plugin Feature Ideas

  • Show SDL in hover tip
  • Use semantic highlighting to color docblock tags
  • Refactor/quickfix suggestions
  • Show SDL in inlay text

Running Grats on Deno

Here's a repo that runs repo and documents some warts and workarounds.

What I have found so far:

  • Scalars provided by Grats (import { Float, Int, ID } from "grats") are importable just fine, but seems like the docblocks aren't reachable.
    • Kinda makes sense: these imports are importmaps managed by Deno; I'm guessing Grat's Typescript compiler can't locate the actual file where the scalars are defined, the file containing the scalars' dockblocks.
    • A not-so-terrible workaround is to "vendor" the scalar definitions, so they are fully accessible by Grat's (see schema.ts in this repo).
  • Module remapping must be done carefully to ensure a single graphql version is pulled, otherwise graphql itself will complain.

Support positional resolver arguments

GraphQL JS expects GraphQL field arguments to be passed to the resolver as a single args object where each argument is a named property. This makes sense in a world where your executor does not know anything about the implementation of your resolvers, but in Grats I think we can offer a more ergonomic alternative: positional arguments.

So, instead of:

/** @gqlField */
export function greeting(_: Query, args: {name: string, greeting: string}): string {
  return `${args.greeting}, ${args.name}`;
}

We could support the simpler:

/** @gqlField */
export function greeting(_: Query, name: string, greeting: string): string {
  return `${greeting}, ${name}`;
}

This becomes especially nice when you want to have descriptions or @deprecated on your args:

/** @gqlField */
export function greeting(
  _: Query,
  /** Name by which to greet the person */
  name: string,
  /** Salutation to use when greeting */
  greeting: string,
  /** @deprecated Prefer the `name` arg */
  userName: string
): string {
  return `${greeting}, ${name}`;
}

Implementation

At extraction time Grats could annotate the fields with directives defining which field name maps to which position. That mapping could then be implemented using the same type of resolver wrapper we use for @methodName.

Context

One reason not to use positional arguments is that it interferes with your non-GraphQL arguments like context and the extra metadata argument. However, if Grats knew which type was your context type (for example via a @gqlContext tag), it could just know which position you were expecting your context in, and pass it in this position using a similar mapping strategy:

/** @gqlContext */
type RequestContext = { username: string };

/** @gqlField */
export function greeting(
  _: Query,
  greeting: string,
  ctx: GqlContext
): string {
  return `${greeting}, ${ctx.username}`;
}

v.s the more verbose

/** @gqlField */
export function greeting(
  _: Query,
  args: {
    /** Name by which to greet the person */
    name: string,
    /** Salutation to use when greeting */
    greeting: string,
    /** @deprecated Prefer the `name` arg */
    userName: string
  }
): string {
  return `${args.greeting}, ${args.name}`;
}

Now even the context object does not need to be defined at a fixed postion. Maybe you prefer getting your context first?

/** @gqlContext */
type RequestContext = { username: string };

/** @gqlField */
export function greeting(
  _: Query,
  ctx: GqlContext,
  greeting: string
): string {
  return `${greeting}, ${ctx.username}`;
}

(On the other hand, maybe having a strong convention around context going after all GraphQL arguments is better for clarity in the code base. In which case that last option might be a bridge too far.)

Needed: Logo

Should be used in:

  • Readme
  • Docs home page
  • Docs favicon
  • Open graph tags on docs site
  • GitHub social preview

Support get methods

import { Float as GqlFloat, Int as GqlInt } from "grats";
/** @gqlScalar */
type GqlDate = Date;

/** 
 * Object representing cooking recipe
 * @gqlType
 */
export class Recipe {
  /** @gqlField */
  title!: string;

  /** 
   * @gqlField
   * @deprecated Use 'description' field instead
   */
  get specification(): string | undefined {
    return this.description;
  }

  /** 
   * The recipe description with preparation info
   * @gqlField
   */
  description?: string;

  /** @gqlField */
  ratings!: GqlInt[];

  /** @gqlField */
  creationDate!: GqlDate;

  /** @gqlField */
  ratingsCount!: GqlInt;

  /** @gqlField */
  get averageRating(): GqlFloat | null {
    const ratingsCount = this.ratings.length;
    if (ratingsCount === 0) {
      return null;
    }
    const ratingsSum = this.ratings.reduce((a, b) => a + b, 0);

    return ratingsSum / ratingsCount;
  }
}

Playground link

Allow Grats to validate context values

The third argument to a resolver is always the request context. This can be anything the user configures, so it's generally hard to typecheck effectively. However, Grats knows each function/method that is a GraphQL field (thanks to the @gqlField annotations) so could be charged with ensuring all fields are typed in a way that matches the developer's expectations.

This would require some way for Grats users to inform users what type they are passing to their executor. I'll need to brainstorm what options make sense here:

  1. Grats config file allows you to define a module path + export name
  2. Some well defined global type (GratsContextValue?) that can be defined by the user in their code and Grats will look for
  3. Some other TypeScript trick that I'm not thinking of?
  4. Edit /** @gqlContext */ on the context type declaration.

We should also examine how existing TS GraphQL servers address this. I'm thinking specifically of TypeGraphQL and Pothos.

Class which extends one interface and implements another only gets the implemented interface in schema definition

Given the following typescript definitions:

/**
 * @gqlInterface Node
 */
export interface Node {
  /** @gqlField */
  id: string;
}

/** @gqlInterface Something */
interface SomethingElse {
  /** @gqlField */
  hello: string;
}

/** @gqlType */
export class User extends Node implements SomethingElse {
  __typename: "User";
  /** @gqlField */
  id: string;
  /** @gqlField */
  hello: string;
  /** @gqlField */
  username: string;
  /** @gqlField */
  email: string;
}

We get the generated type

type User implements SomethingElse {
  email: String
  hello: String
  id: String
  username: String
}

But if we switch User to implement both Node and SomethingElse rather than extending one, we get both correctly added to the generated schema

type User implements Node & SomethingElse {
  email: String
  hello: String
  id: String
  username: String
}

Given that both extends and implements are supported for interfaces, I would expect both to work when applied to the same class.

Unsure how to do mutations for Class resolvers.

Is there a recommended way? I can't find anything in the docs.

I tried making a Root class and used that as the root resolver.

The Root class contains a Mutation and a Query class.

Except this doesn't work.

The example express app you have only uses Query.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.