Giter Site home page Giter Site logo

helios1138 / graphql-typed-client Goto Github PK

View Code? Open in Web Editor NEW
214.0 3.0 18.0 306 KB

A tool that generates a strongly typed client library for any GraphQL endpoint. The client allows writing GraphQL queries as plain JS objects (with type safety, awesome code completion experience, custom scalar type mapping, type guards and more)

License: MIT License

TypeScript 98.44% JavaScript 1.56%
graphql typescript javascript typings client code-generation type-annotations dts dtsfiles codegen

graphql-typed-client's Introduction

graphql-typed-client npm version Build Status

A tool that generates a strongly typed client library for any GraphQL endpoint. The client allows writing GraphQL queries as plain JS objects (with type safety, awesome code completion experience, custom scalar type mapping, type guards and more)

Chain request syntax

Raw request syntax

The request above is then converted to the GraphQL query and variables

query Query($v1: String!, $v2: SearchType!, $v3: Int) {
  search(query: $v1, type: $v2, first: $v3) {
    nodes {
      ...f3
    }
  }
}
fragment f1 on User {
  name
}
fragment f2 on Organization {
  name
}
fragment f3 on Repository {
  name
  owner {
    ...f1
    ...f2
  }
}
{ "v1": "graphql", "v2": "REPOSITORY", "v3": 5 }

Install

yarn global add graphql-typed-client # needed for the CLI to work globally
yarn add graphql-typed-client # needed for the generated client to work

Generate the client

To generate the client, use the CLI tool

generate-graphql-client
Usage: generate-graphql-client [options]

Options:
  -o, --output <./myClient>                    output directory
  -e, --endpoint <http://example.com/graphql>  GraphQL endpoint
  -p, --post                                   use POST for introspection query
  -s, --schema <./*.graphql>                   glob pattern to match GraphQL schema definition files
  -f, --fetcher <./schemaFetcher.js>           path to introspection query fetcher file
  -c, --config <./myConfig.js>                 path to config file
  -v, --verbose                                verbose output
  -h, --help                                   output usage information

If your endpoint is able to respond to introspection query without authentication, provide the endpoint option (use post option to use POST request)

generate-graphql-client -e http://example.com/graphql -o myClient

# or using POST
generate-graphql-client -e http://example.com/graphql -p -o myClient

If your endpoint requires authentication or maybe some custom headers, use fetcher option to provide a custom fetcher function. We will pass fetch and qs instances to your function for convenience, but you can use anything you like to fetch the introspection query

generate-graphql-client -f customFetcher.js -o myClient
// customFetcher.js

module.exports = function(query, fetch, qs) {
  return fetch(`https://api.github.com/graphql?${qs.stringify({ query: query })}`, {
    headers: {
      Authorization: 'bearer YOUR_GITHUB_API_TOKEN',
    },
  }).then(r => r.json())
}

If instead of making a query to some endpoint, you just want to use a GraphQL schema definition, use schema option

generate-graphql-client -s mySchema.graphql -o myClient

# or
generate-graphql-client -s *.graphql -o myClient

# this will also work
generate-graphql-client -s "type User { name: String } type Query { users: [User] }" -o myClient

Alternatively, you can use a JS or JSON config file to define how you want the client to be generated. Also, using the config file you can define more than one client.

The config file should contain an object or an array of objects, each representing a client to be generated. Object fields are named the same way as the CLI arguments described above + options field for passing various parsing/generation options (see config.ts to learn more)

generate-graphql-client -c myConfig.js
// myConfig.js

module.exports = [
  {
    schema: 'type Query { hello: String }',
    output: 'clients/simpleClient',
  },
  {
    schema: 'schemas/**/*.graphql',
    output: 'clients/clientFromSchema',
  },
  {
    endpoint: 'http://example.com/graphql',
    post: true,
    output: 'clients/exampleClient',
  },
  {
    fetcher: 'customFetcher.js',
    output: 'clients/customClient',
  },
  {
    fetcher: (query, fetch, qs) =>
      fetch(`https://api.github.com/graphql?${qs.stringify({ query })}`, {
        headers: {
          Authorization: 'bearer YOUR_GITHUB_API_TOKEN',
        },
      }).then(r => r.json()),
    output: 'clients/githubClient',
  },
]

Create the client instance

To create the client instance, you have to call createClient() function that was generated with the client

If you want to execute Queries and Mutations, provide a fetcher function.

Just like with the fetcher that can be used for client generation, we will pass fetch and qs instances inside for convenience, but the function can be implemented in any way you want

If you want to execute Subscriptions, provide subscriptionCreatorOptions object with uri and options fields, where options are ClientOptions passed down to subscriptions-transport-ws (reconnect and lazy options are already enabled by default)

// myClient.js

import { createClient } from './clients/myClient/createClient'

export const myClient = createClient({
  fetcher: ({ query, variables }, fetch, qs) =>
    fetch(`http://example.com/graphql?${qs.stringify({ query, variables })}`, {
      headers: {
        Authorization: 'bearer MY_TOKEN',
      },
    }).then(r => r.json()),
  subscriptionCreatorOptions: {
    uri: 'wss://example.com/graphql-subscriptions',
    options: {
      connectionParams: {
        token: 'MY_TOKEN',
      },
    },
  },
})

Making GraphQL requests in JS

Raw request syntax

The format for the request object is visually similar to an actual GraphQL query, so something like

query({
  user: [{ id: 'USER_ID' }, {
      username: 1,
      email: 1,
      on_AdminUser: {
        isSuperAdmin: 1,
      },
  }],
})

is easily recognizable as

query {
  user(id: "USER_ID") {
    username
    email
    ... on AdminUser {
      isSuperAdmin
    }
  }
}

Here are the rules governing the format:

  • fields with scalar types are written as

    name: 1 or name: true

  • fields with object types are written as JS objects

    user: { name: 1 }

  • fields that have arguments are written as arrays with argument object and the field selection

    user: [{ id: 'USER_ID' }, { name: 1 }]

    • if the field has arguments, but the return type is scalar, just pass the array with argument object

      userCount: [{ status: 'active' }]

    • if all the arguments for the field are optional, you can omit the array and just pass the field selection

      friend: { name: 1 } is the same as friend: [{}, { name: 1 }]

  • fields with union or interface types can have fragments defined on them to select fields of a specific type

    on_AdminUser: { superAdmin: 1 }

  • additionally, there is a special __scalar field, that can be included in the field selection to automatically include all scalar fields from an object/interface type (excluding __typename, which you have to request manually if you need it)

    user: { __scalar: 1 }

Here is an example request object, showing all possible field types

myClient.query({
  user: [{ id: 'USER_ID' }, {
    username: 1,
    email: 1,
    wasEmployed: [{ recently: true }],
    friends: {
      username: 1,
      email: 1,
    },
    posts: [{ limit: 5 }, {
      __scalar: 1,
    }],
    pets: {
      name: 1,
      on_Cat: {
        eyeColor: 1,
      },
      on_Snake: {
        length: 1,
      },
    },
  }],
})

When executed, it will send the following GraphQL query and variables to the server

{ "v1": "USER_ID", "v2": true, "v3": 5 }
query($v1: ID!, $v2: Boolean, $v3: Int) {
  user(id: $v1) {
    username
    email
    wasEmployed(recently: $v2)
    friends {
      username
      email
    }
    posts(limit: $v3) {
      ...f1
    }
    pets {
      name
      ...f2
      ...f3
    }
  }
}
fragment f1 on Post {
  id
  title
  content
}
fragment f2 on Cat {
  eyeColor
}
fragment f3 on Snake {
  length
}

Chain request syntax

myClient.chain.query.user({ id: 'USER_ID' }).execute({
  username: 1,
  email: 1,
  on_AdminUser: {
    isSuperAdmin: 1,
  },
})

// execute() returns Promise<User> on query/mutation and Observable<User> on subscription

In the chain, each member refers to a GraphQL field going down the tree. Fields with arguments can be called like methods. You can continue the chain so long as the fields that are mentioned are object types or interfaces (not arrays, unions etc.). At any point, you can finish the chain by calling execute(fieldRequest, defaultValue). Calling execute() returns a Promise (for query/mutation) or an Observable (subscription) of type equal to the type of the last field in the chain. Unlike in raw request syntax, where GraphQL errors are just returned in the response, chain execution will throw an error if GraphQL endpoint responds with errors, empty data or empty value at the requested path when no defaultValue was provided.

Default value logic clarification
type User {
  status: String
}
const status = await myClient.chain.query.user({ id: 'USER_ID' }).status.execute()
// status is `String | null`, which means that if user with specified ID exists, any string or null
// are both considered valid
// but if the user with specified ID is not found, the Promise returned from `execute()`
// will throw an error

const status = await myClient.chain.query.user({ id: 'USER_ID' }).status.execute(1, 'default status')
// in this case, if the user with specified ID is not found, returned status will be 'default status'

Custom scalar type mapping

By default, all custom scalar types are generated as aliases to TypeScript's any

You can override this behavior by providing your own type mapper that will be used during the schema generation and applied to query responses

For example, let's say you have a custom Date type. To specify how this type should be serialized/deserialized, create a type mapper file (.ts or .js) somewhere in your app

// path/to/typeMapper.ts

import moment, { Moment } from 'moment'

export const typeMapper = {
  Date: {
    serialize: (date: Moment) => date.toISOString(),
    deserialize: (date: string) => moment(date),
  },
}

Add typeMapper option to client generation config

module.exports = {
  endpoint: 'http://example.com/graphql',
  output: 'clients/myClient',
  options: { typeMapper: { location: 'path/to/typeMapper', types: ['Date'] } },
}

Now, all fields of Date type in query responses will be automatically deserialized, and the return type of the deserialize() function is going to be used as the definition for Date in generated TypeScript (enabling correct code completion and type checking). All query variables of Date type and input object types that have Date fields will be automatically serialized before sending.

myClient
  .query({
    // `activatedAfter` is a `Date` argument, so now it accepts `Moment` instances
    user: [{ id: 'USER_ID', activatedAfter: moment('1999-01-01') }, {
      name: 1,
      birthday: 1,
    }],
  })
  .then(result => {
    if (!result.data) return
    const user = result.data.user

    // moment's methods are now available for `birthday` field in the response
    console.log(user.birthday.startOf('day').toISOString())
  })

Type guards

Additionally, Typescript type guard functions are generated for every object, interface and union type in your schema

import { isCat, isSnake } from './clients/testClient/schema'

myClient
  .query({
    pet: [{ id: 'PET_ID' }, {
      name: 1,
      on_Cat: { eyeColor: 1 },
      on_Snake: { length: 1 },
    }],
  })
  .then(result => {
    if (!result.data) return
    const pet = result.data.pet

    console.log(pet.name) // pet type is abstract type Pet, so you only get access to shared fields

    if (isCat(pet)) {
      console.log(pet.eyeColor) // pet type is Cat, so you get access to fields of the specific type
    } else if (isSnake(pet)) {
      console.log(pet.length) // same here
    }
  })

Notes on type annotation generation

  • all known Scalar types are converted to their Typescript counterparts
  • all unknown Scalar types are converted to type aliases for any unless type mapper is provided
  • all Enum types are converted to Typescript enums, so you can import and use them in your code (even if you're not using Typescript)

Notes on subscriptions

The generated client uses Apollo's subscriptions-transport-ws for executing Subscriptions

Subscriptions are wrapped in RxJs' Observable which is chained to the SubscriptionClient so that a connection is opened when you subscribe to the first subscription, shared among all subscriptions and closed when you unsubscribe from the last one.

graphql-typed-client's People

Contributors

capaj avatar helios1138 avatar tonyfromundefined 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

graphql-typed-client's Issues

Question: Is schema.json absolutely necessary?

I tried to generate the client for our schema at @LeapLabs and the json is pretty big- around 270 Kb.
Is this JSON necessary?
I guess the library generates the GQL query based on it, so it most likely is. It just seems like it's not going to work well for large and complex APIs if you have to load up hundred of kilobytes of JSON just to do a small query.

add alternative chain-query syntax based on Proxy

ability to write something like

await client.query
  .search({ query: 'graphql', type: SearchType.REPOSITORY, first: 5 })
  .nodes
  .on_Repository({
    name: 1,
  })
  .execute()

as alternative to

await client.query({
  search: [{ query: 'graphql', type: SearchType.REPOSITORY, first: 5 }, {
    nodes: {
      on_Repository: {
        name: 1,
      },
    },
  }],
})

exact synthax subject to change

Serialization of array of custom scalars fails

I created custom Time type map like this:

  Time: {
    serialize: (time: moment.Moment) => time.format("HH:mm:ss"),
    deserialize: (time: string) => moment(time, "HH:mm:ss"),
  }

When mutation accepts single Time value, it works ok. But when it accepts array ([Time!]), serialize function receives array of moment objects instead of getting them one by one.

On the screenshot below, time does not have format because time is array of moments, not moment object.

image

Mutation is called like this:

graphqlClient.chain.mutation
  .userProfileUpdate({input: {times: [
    moment("08:00", "HH:mm"),
    moment("14:00", "HH:mm"),
    moment("20:00", "HH:mm"),
  ]}})

Generated code can have implicit any

For all unions/interfaces/objects in the GraphQL schema, the generated code includes a "type-guard" called is{name}, which uses a string array {name}_possibleTypes with the type-names implementing the source value. That array implicitly typed, however.

If the schema includes unused interfaces, the array will be empty, and the array is implicitly typed as any[]. If the Typescript compiler setting noImplicitAny is used, that is a compile error.

Feature request: request batching

would be nice if this library had support for query batching.

We currently use apollo-client and it's query batching strategy is to wait with any query fox X ms and if there are any consecutive it just adds them into array and dispatches the array to the server I think. So it should be trivial.

Reduce bundle size

Currently the bundle size is pretty big because of lodash and rxjs https://bundlephobia.com/[email protected]

Lodash impact can be reduced simply by changing the import paths, rxjs instead requires the consumer tree shaking the library to reduce bundle size

To enable tree shaking this library should expose a module version of the lib and generate the entry point code with the import syntax instead of require, this way Webpack can tree shake the unused rxjs operators

GraphQL Aliases

Is there anyway to specify an alias in the query object?

My use case - I have a list of product ID's that I want to fetch from shopify. I want to fetch the products all at once, so would usually programatically create a gql string like so:

query products {
  [id]: product (...) {..}
  [id]: product (...) {..}
  [id]: product (...) {..}
  [id]: product (...) {..}
  ...
}

I can create an object similar to the above to pass into query, but obviously can't have multiple product: [{}] as they are duplicate keys. Or perhaps there is a way to create a chain with a number of queries?

Hope that makes sense..?

graphql subscription is not working

i have deployed my hasura app on hasura-cloud, and trying to call a subscription using the generated typed client, but somehow there is no log for the subscription on the client.

image

this is my query in graphql console, working fine, but requesting through a chained query is not really working

app.listen(PORT, () => {
	console.log('Server is listening on port :', PORT)
    console.log('adding subscriptions');
    graphqlClient.chain.subscription.users().execute({email:1,id:1,created_at:1}).subscribe((data)=>{
        console.log('subscription users->', data);
    })
})

Feature request: Custom deserialization of scalars/object types/interface types

it would be nice to be able to associate scalars/object types/interface types to classes at runtime.

So currently the whole client is build statically so it's impossible, but ideally I could do something like:

class User {
  get fullName () {
     return this.firstName + ' ' + this.lastName
  }
}

const myCustomClient = graphqlTypedClient({DateTime: Moment, User: User})

const {data} = myCustomClient.query({currentUser: {firstName: 1, lastName: 1}})
// if this returns {data: {currentUser: {firstName: 'Frodo', lastName: 'Baggins'}}}
console.log(data.currentUser.fullName) // would give me the user's full name-so 'Frodo Baggins'

also being able to define a function as custom deserializer would be beneficial. So not just a class.

Execution result type includes the full schema

It would be very nice if the result type only included the selected fields.
I imagine this could be solvable by using a combination of mapped types and conditional types.

Have you looked into this at and have some experience on the possibility?

Strong typed return value

graphql-zeus was able to strong type return result will using pretty much the same syntax as this library. Would be nice to have that support as well.

Question/FR: How to get graphql query string?

First of all thanks for graphql-typed-client.

I want to use a specific feature thats to convert from JS query/mutation object to GQL string.

I skimmed through the documentation, but did not find anything that would allow me to do just that.

I am specifically looking for.

myClient.query({
  user: [{ id: 'USER_ID' }, {
      username: 1,
      email: 1,
      on_AdminUser: {
        isSuperAdmin: 1,
      },
  }],
}).generateGQL()

I have my own client that I'd like to use to send queries to server.

Comparison of this library with graphql-zeus?

graphql-zeus seems like a library that is similar to this one.

What advantages does this library have over graphql-zeus?

zeus seems to have solved some issues that this library has: #12 and #31

On the other hand, this library seems have "custom scalar type mapping" feature which zeus is lacking.

I don't mean any disrespect, I am evaluating libraries trying to decide which to use and wondering what advantages these have relative to each other. Has anyone done a comparison?

Custom scalar type mapping does not work with relative paths on Windows

The example in the README for a custom scalar type mapping does not work on Windows.

module.exports = {
  endpoint: 'http://example.com/graphql',
  output: 'clients/myClient',
  options: { typeMapper: { location: 'path/to/typeMapper', types: ['Date'] } },
}

The problem is that the output and typeMapper's location are different, so a relative path will be generated (in relativeImportPath.ts). In this case, the relative path would be '../../path/to/typeMapper'

However, the code uses path.relative() to generate that path, which is OS-dependant: Node documentation. On Windows, this will generate the string '..\..\path\to\typeMapper', which is not a valid Node import string. Instead, use path.posix.relative() to force POSIX-compliant paths, even on Windows.

Prettier dependency

Hi, awesome library, I think it boasts the best interface for graphql clients!
Though, when building a lambda function, I noticed that 'prettier' is being used within the library and will thus be bundled by webpack. Is prettier really necessary in client runtime?

I'm asking, since prettier is very heavy and will make the build process considerably slower, at least in my case.

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.