Code Monkey home page Code Monkey logo

tql's Introduction

Warning: This library will not be recieving updates any time soon.

🙏 Thank you to all contributors. I am not interested in maintaining this right now but please feel free to fork and take inspiration from!

TQL

tql is a TypeScript GraphQL query builder.

  • 🔒 Fully Type-safe - Operation results and variables are fully type-safe thanks to TypeScript's advanced type-system.
  • 🔌 Backendless: - Integrate with any GraphQL client to execute queries.
  • 🔮 Automatic Variables: - Variable definitions are automatically derived based on usage.
  • 📝 Inline Documentation: JSDoc comments provide descriptions and deprecation warnings for fields directly in your editor.
  • Single Dependency: graphql-js is our single runtime (peer) dependency.

Try out our pre-compiled Star Wars GraphQL SDK on CodeSandbox!

Installation

  1. npm install @timkendall/tql@beta

  2. Generate an SDK with npx @timkendall/tql-gen <schema> -o sdk.ts

<schema> can be a path to local file or an http endpoint url.

Usage

Import selector functions to start defining queries 🎉

import { useQuery } from '@apollo/client'

// SDK generated in previous setup
import { character, query, $ } from './starwars'

// define reusable selections
const CHARACTER = character(t => [
  t.id(),
  t.name(),
  t.appearsIn(),
])

const QUERY = query((t) => [
  t.reviews({ episode: Episode.EMPIRE }, (t) => [
    t.stars(),
    t.commentary(),
  ]),

  t.human({ id: $('id') }, (t) => [
    t.__typename(),
    t.id(),
    t.name(),
    t.appearsIn(),
    t.homePlanet(),

    // deprecated field should be properly picked-up by your editor
    t.mass(),

    t.friends((t) => <const>[
      t.__typename(),
      
      ...CHARACTER,
      // or
      CHARACTER.toInlineFragment(),

      t.on("Human", (t) => [t.homePlanet()]),
      t.on("Droid", (t) => [t.primaryFunction()]),
    ]),

    t.starships((t) => [t.id(), t.name()]),
  ]),
]).toQuery({ name: 'Example' })

// type-safe result and variables 👍
const { data } = useQuery(QUERY, { variables: { id: '1011' }})

Inspiration

I was inspired by the features and DSL's of graphql-nexus, graphql_ppx, gqless, and caliban.

License

MIT

tql's People

Contributors

dependabot[bot] avatar lorefnon avatar nathanchapman avatar ryb73 avatar timkendall 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

tql's Issues

Restrict `Selector` methods to returning appropriate `Field`'s

We have the power to restrict what types of Field's are returned (i.e "selected") from a Selector object's method.

Some possible implementation options:

  • Inline define union of field names (as shown here)
  • Define a union of the entire selection available to a type (ex. type UserSelection = Array<Field<'id', [], string> | Field<'friends', [], any>>>) (this might be the key to getting nullable / list type support)
  • Note: This is also a critical piece for supporting "native" fragments

Abstract type support

Interfaces and unions are powerful tools for modeling many domains and a full-featured GraphQL client should fully support them. Here are some ideas on how we accomplish that here.

Interfaces:

  • Define special interface Selector objects that have __typename and on methods (as well as common interface fields)
  • __typename returns a Field of type string literal (enumerating all of the interface implementations in the schema)
  • on takes two arguments, a string literal (enumerating implementations) and a selector callback allowing selection on a concrete type
  • Define exported guard functions for utility
  • Somehow type return as discriminating union

Example:

query(“Example”, t => 
  t.accounts(t => 
     t.__typename(),
     t.on(“InvestmentAccount”, t => [ t.broker() ]),
     t.on(“RetirementAccount”, t => [ t.age() ]),
  )
)

Note: This will likely lead us to implementing fragments (to do properly at least).

Type errors in generated TS file

Typescript output:

[11:13:13 PM] Starting compilation in watch mode...

graphql-api.ts:5839:22 - error TS2552: Cannot find name 'Query'. Did you mean 'query'?

5839   select: (t: typeof Query) => T
                          ~~~~~

graphql-api.ts:5841:56 - error TS2552: Cannot find name 'Query'. Did you mean 'query'?

5841   new Operation(name, "query", new SelectionSet(select(Query)));
                                                            ~~~~~

graphql-api.ts:5845:22 - error TS2552: Cannot find name 'Mutation'. Did you mean 'mutation'?

5845   select: (t: typeof Mutation) => T
                          ~~~~~~~~

graphql-api.ts:5847:59 - error TS2552: Cannot find name 'Mutation'. Did you mean 'mutation'?

5847   new Operation(name, "mutation", new SelectionSet(select(Mutation)));
                                                               ~~~~~~~~

graphql-api.ts:5851:22 - error TS2552: Cannot find name 'Subscription'. Did you mean 'subscription'?

5851   select: (t: typeof Subscription) => T
                          ~~~~~~~~~~~~

graphql-api.ts:5853:63 - error TS2552: Cannot find name 'Subscription'. Did you mean 'subscription'?

5853   new Operation(name, "subscription", new SelectionSet(select(Subscription)));
                                                                   ~~~~~~~~~~~~

[11:13:20 PM] Found 6 errors. Watching for file changes.

Full graphql-api.ts: https://gist.github.com/ryb73/def4d02805995f953b37f3f8405df533

Generated using command:

yarn --silent tql http://localhost:8080/v1/graphql > graphql-api.ts

I'm using a GraphQL server generated by Hasura.

It looks like it didn't generate Query, Mutation, or Subscription? Is there some configuration I need to do before running tql?

Named fragment API

Possible API:

// or class-based API

const userName = new Fragment({
  name: 'UserDetails',
  on: 'User',
  selection: (t) => [ t.name(t => [ t.first(), t.last() ]) ]
})

// higher-level functional API
const userName = on('User', t => [ t.name(t => [ t.first(), t.last() ])).named('UserNameFields')
// or
const userName = user(t => [ t.name(t => [ t.first(), t.last() ])).toFragment('UserNameFields')

Client and HTTPTransport class improvements

  • Unwrap data object (i.e so we don't have to do data.hero...)
  • HTTP Keep-alive by default
  • Persisted query support
  • Structured logging support
  • Exponential backoff retry
  • Log warning on deprecated field usage

Cool project!

Hey! 👋🏿

I couldn't find your email or Twitter link to reach out - sorry for opening an issue just for that.

I really like your project! I am working on something very, very similar (Kayu) - do you think we could maybe merge the projects? I don't think it's beneficial to solve the same problem twice. I really like the idea of using TS to define queries and especially your approach to using them with urql and Apollo client (as"not writing alternative clients").

Well, let me know if you want to chat! I think we could learn a lot from each other. 😄

Define Result types as readonly

We should make return types readonly and immutable (maybe even at runtime with Object.freeze) as a best practice (with the ability to opt-out and disable runtime Object.freeze).

Variable definition support

Variable definitions allow operations to be reused.

query(v => [ v.definition(“id”, v.string) ], t => [ t.account({ id: new Variable(“id”) }, t => [ t.balance() ]))

Higher level API:

import { query, $, VariablesOf } from '@timkendall/tql'

const a = query<any>(t => [
  t.user({ id: $`id!` }, t => [ t.id() ])
])

type AVariables = VariablesOf<typeof a> // { string: any }

InputObjectField's of Enum values serialization

Inline field arguments of input objects with enum fields are not serialized correctly.

Example:

mutation('MyMutation', t => [
  // @bug SomeEnum.FOO is serialized as a string!
  t.myMutation({ input: { someEnum: SomeEnum.FOO }, t => [ 
    t.__typename(),
  ])
])

Add benchmarks

  • Node.js CPU and memory usage event-loop delay
  • Operations per second (using benchmark.js/benny)
  • Against other "fluent" GraphQL libraries

Compiler plugin

As a runtime optimization it would be nice to have compiler plugins for TypeScript, Babel, ESBuild, etc. to replace the function invocations with static query text (to avoid both the cost of DSL function calls and GraphQL AST serialization).

We would need to account for cases where users are doing "bad things" and dynamically generating queries (perhaps a linter plugin and/or warning emitted from the compiler could be solutions).

We could also experiment with support for "Build-time APQ's"; i.e replace query text with a SHA and export operations to be pre-registered/whitelisted with an API server.

Examples:
-https://github.com/xialvjun/ts-sql-plugin
-https://dev.doctorevidence.com/how-to-write-a-typescript-transform-plugin-fc5308fdd943

Operation API v2

Riffing off of the new Selector API's let's clean-up the type-safe Query, Mutation, and Subscription selectors that we generate.

Examples:

import { query, mutation, subscription, t, $ } from './operations'

const a =  new Query({
  name: 'Foo',
  variables: { id: t.string },
  directives: [/* @todo */]
  selection: (t, v) => [
    t.user({ id: v.id }, t => [
      t.firstName(),
    ])
  ],
  extensions: [/* @todo */],
})

// or simpler...maybe just for anonymous + no-variables?

const b = query<'Example'>(t => [
  t.viewer(t => [
    t.id(),
    t.avatar()
  ]),

  // with a variable…
  t.user({ id: $`foo` }, t => [ t.id() ])
])

Add docs on SDK building

From the README it is not immediately obvious how to build a type-safe SDK with this library/tool. Perhaps we should implement a separate CLI (create-graphql-sdk?) or command that drives this workflow?

Considerations for SDK-building workflows:

  1. Generating of supporting files and structure (like package.json, src/, etc.)
  2. Splitting up the generated code into separate modules for larger API's (for readability, build performance, and easier code-splitting)
  3. Generating the appropriate semantic version for the SDK based on the previous graph and tql runtime version
  4. (Optional) Generating a changelog
  5. (Optional) Generating documentation (via. GitBook, Docusaurus, etc.)
  6. (Optional) Publishing to package registries

Type-check Output

We should run the TypeScript type-checker on the generated output (Prettier already runs the parser). Relates to #46

Convenience Client class generation

In-order-to help with ease of use, we could generate a thin Client class wrapper around our query, mutation, and subscription primitives. It would expose methods for each top-level operation (as well-as expose more flexible query and mutation methods).

Example (with Starwars schema):

import { Transport, Operation } from '@timkendall/tql'

export class Starwars {
  constructor(private readonly transport: Transport) {}
  
  public viewer(select: ViewerSelector) {}

  public addReview(variables: AddReviewInput, select: AddReviewSelector ) {}

  public query(name: string, select: QuerySelector) {}

  public mutate(name: string, select: MutationSelector) {}

  private execute<T>(operation: Operation<T>) {
    //  execute GraphQL operation against `this.transport`
  }
}

Add Fig autocompletion spec

Might be nice to offer an integration with Fig for VSCode-style terminal autocompletion (distribution seems to be as easy as storing a new .js module in ~/.fig/autocomplete; it looks like they might have some package.json integration).

// To learn more about Fig's autocomplete standard visit: https://fig.io/docs/autocomplete/building-a-spec#building-your-first-autocomplete-spec

// The below is a dummy example for git. Make sure to change the file name!
export const completion: Fig.Spec = {
  name: "tql",
  description: "Generate a fluent TypeScript client for your GraphQL API.",
  args: [
    { 
      name: 'schema',
      description: 'GraphQL schema from HTTP endpoint or local .graphql file.',

      // @todo discover a local dir schema via. .graphqlconfig?
      // generators: [
      //   {
      //     script: "git branch -l",
      //     postProcess: function(out) {
      //       return out.split('\n').map(branch => {
      //         return { name: branch, description: "branch"}
      //       })
      //     }
      //   }
      // ]
    }
  ],
  subcommands: [],
  options: [
    {
      name: ["-v", "--version"],
      description: "View tql version",
    },
    {
      name: ["-c", "--client"],
      description: "Generate an SDK client for the API",
    },
    {
      name: ["-t", "--tag"],
      description: 'Semantic versioning tag, ex. "1.0.0" (defaults to "unversioned")'
    },
    {
      name: ['--mutableFields'],
      description: 'Generate schema types and results as mutable (default is read-only).',
    },
    {
      name: '--module-path',
      description: 'Optional path to the @timkendall/tql module to be included (CDN, private registry, etc.).'
    }
  ],
};

Root-level scalar and enum fields have Selector's

Our code generator incorrectly generates Selector objects for root-level (i.e Query, Mutation, Subscription) type scalar fields. Example:

 suspendDeposits: <T extends Array<Selection>>(
      variables: { suspended?: boolean },
      select: (t: VoidSelector) => T
    ) =>
      this.executor.execute<
        IMutation,
        Operation<
          SelectionSet<[Field<"suspendDeposits", any, SelectionSet<T>>]>
        >
      >(
        new Operation(
          "suspendDeposits",
          "mutation",
          new SelectionSet([Mutation.suspendDeposits<T>(variables, select)])
        )
      ),

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.