Git Product home page Git Product logo

typed-graphqlify's Introduction

release test npm version codecov

image

typed-graphqlify

Build Typed GraphQL Queries in TypeScript. A better TypeScript + GraphQL experience.

Install

npm install --save typed-graphqlify

Or if you use Yarn:

yarn add typed-graphqlify

Motivation

We all know that GraphQL is so great and solves many problems that we have with REST APIs, like overfetching and underfetching. But developing a GraphQL Client in TypeScript is sometimes a bit of pain. Why? Let's take a look at the example we usually have to make.

When we use GraphQL library such as Apollo, We have to define a query and its interface like this:

interface GetUserQueryData {
  getUser: {
    id: number
    name: string
    bankAccount: {
      id: number
      branch?: string
    }
  }
}

const query = graphql(gql`
  query getUser {
    user {
      id
      name
      bankAccount {
        id
        branch
      }
    }
  }
`)

apolloClient.query<GetUserQueryData>(query).then(data => ...)

This is so painful.

The biggest problem is the redundancy in our codebase, which makes it difficult to keep things in sync. To add a new field to our entity, we have to care about both GraphQL and TypeScript interface. And type checking does not work if we do something wrong.

typed-graphqlify comes in to address this issues, based on experience from over a dozen months of developing with GraphQL APIs in TypeScript. The main idea is to have only one source of truth by defining the schema using GraphQL-like object and a bit of helper class. Additional features including graphql-tag, or Fragment can be implemented by other tools like Apollo.

How to use

Define GraphQL-like JS Object:

import { query, types, alias } from 'typed-graphqlify'

const getUserQuery = query('GetUser', {
  user: {
    id: types.number,
    name: types.string,
    bankAccount: {
      id: types.number,
      branch: types.optional.string,
    },
  },
})

Note that we use our types helper to define types in the result.

The getUserQuery has toString() method which converts the JS object into GraphQL string:

console.log(getUserQuery.toString())
// =>
//   query getUser {
//     user {
//       id
//       name
//       bankAccount {
//         id
//         branch
//       }
//     }
//   }

Finally, execute the GraphQL and type its result:

import { executeGraphql } from 'some-graphql-request-library'

// We would like to type this!
const data: typeof getUserQuery.data = await executeGraphql(getUserQuery.toString())

// As we cast `data` to `typeof getUserQuery.data`,
// Now, `data` type looks like this:
// interface result {
//   user: {
//     id: number
//     name: string
//     bankAccount: {
//       id: number
//       branch?: string
//     }
//   }
// }

image

Features

Currently typed-graphqlify can convert these GraphQL features:

  • Operations
    • Query
    • Mutation
    • Subscription
  • Inputs
    • Variables
    • Parameters
  • Data structures
    • Nested object query
    • Array query
  • Scalar types
    • number
    • string
    • boolean
    • Enum
    • Constant
    • Custom type
    • Optional types, e.g.) number | undefined
  • Fragments
  • Inline Fragments

Examples

Basic Query

query getUser {
  user {
    id
    name
    isActive
  }
}
import { query, types } from 'typed-graphqlify'

query('getUser', {
  user: {
    id: types.number,
    name: types.string,
    isActive: types.boolean,
  },
})

Or without query name

query {
  user {
    id
    name
    isActive
  }
}
import { query, types } from 'typed-graphqlify'

query({
  user: {
    id: types.number,
    name: types.string,
    isActive: types.boolean,
  },
})

Basic Mutation

Use mutation. Note that you should use alias to remove arguments.

Note: When Template Literal Type is supported officially, we don't have to write alias. See #158

mutation updateUserMutation($input: UserInput!) {
  updateUser: updateUser(input: $input) {
    id
    name
  }
}
import { mutation, alias } from 'typed-graphqlify'

mutation('updateUserMutation($input: UserInput!)', {
  [alias('updateUser', 'updateUser(input: $input)')]: {
    id: types.number,
    name: types.string,
  },
})

Or, you can also use params helper which is useful for inline arguments.

import { mutation, params, rawString } from 'typed-graphqlify'

mutation('updateUserMutation', {
  updateUser: params(
    {
      input: {
        name: rawString('Ben'),
        slug: rawString('/ben'),
      },
    },
    {
      id: types.number,
      name: types.string,
    },
  ),
})

Nested Query

Write nested objects just like GraphQL.

query getUser {
  user {
    id
    name
    parent {
      id
      name
      grandParent {
        id
        name
        children {
          id
          name
        }
      }
    }
  }
}
import { query, types } from 'typed-graphqlify'

query('getUser', {
  user: {
    id: types.number,
    name: types.string,
    parent: {
      id: types.number,
      name: types.string,
      grandParent: {
        id: types.number,
        name: types.string,
        children: {
          id: types.number,
          name: types.string,
        },
      },
    },
  },
})

Array Field

Just add array to your query. This does not change the result, but TypeScript will be aware the field is an array.

query getUsers {
  users: users(status: "active") {
    id
    name
  }
}
import { alias, query, types } from 'typed-graphqlify'

query('getUsers', {
  [alias('users', 'users(status: "active")')]: [{
    id: types.number,
    name: types.string,
  )],
})

Optional Field

Add types.optional or optional helper method to define optional field.

import { optional, query, types } from 'typed-graphqlify'

query('getUser', {
  user: {
    id: types.number,
    name: types.optional.string, // <-- user.name is `string | undefined`
    bankAccount: optional({      // <-- user.bankAccount is `{ id: number } | undefined`
      id: types.number,
    }),
  },
}

Constant field

Use types.constant method to define constant field.

query getUser {
  user {
    id
    name
    __typename # <-- Always `User`
  }
}
import { query, types } from 'typed-graphqlify'

query('getUser', {
  user: {
    id: types.number,
    name: types.string,
    __typename: types.constant('User'),
  },
})

Enum field

Use types.oneOf method to define Enum field. It accepts an instance of Array, Object and Enum.

query getUser {
  user {
    id
    name
    type # <-- `STUDENT` or `TEACHER`
  }
}
import { query, types } from 'typed-graphqlify'

const userType = ['STUDENT', 'TEACHER'] as const

query('getUser', {
  user: {
    id: types.number,
    name: types.string,
    type: types.oneOf(userType),
  },
})
import { query, types } from 'typed-graphqlify'

const userType = {
  STUDENT: 'STUDENT',
  TEACHER: 'TEACHER',
}

query('getUser', {
  user: {
    id: types.number,
    name: types.string,
    type: types.oneOf(userType),
  },
})

You can also use enum:

Deprecated: Don't use enum, use array or plain object to define enum if possible. typed-graphqlify can't guarantee inferred type is correct.

import { query, types } from 'typed-graphqlify'

enum UserType {
  'STUDENT',
  'TEACHER',
}

query('getUser', {
  user: {
    id: types.number,
    name: types.string,
    type: types.oneOf(UserType),
  },
})

Field with arguments

Use params to define field with arguments.

query getUser {
  user {
    id
    createdAt(format: "d.m.Y")
  }
}
import { query, types, params, rawString } from 'typed-graphqlify'

query('getUser', {
  user: {
    id: types.number,
    createdAt: params({ format: rawString('d.m.Y') }, types.string),
  },
})

Multiple Queries

Add other queries at the same level of the other query.

query getFatherAndMother {
  father {
    id
    name
  }
  mother {
    id
    name
  }
}
import { query, types } from 'typed-graphqlify'

query('getFatherAndMother', {
  father: {
    id: types.number,
    name: types.string,
  },
  mother: {
    id: types.number,
    name: types.number,
  },
})

Query Alias

Query alias is implemented via a dynamic property.

query getMaleUser {
  maleUser: user {
    id
    name
  }
}
import { alias, query, types } from 'typed-graphqlify'

query('getMaleUser', {
  [alias('maleUser', 'user')]: {
    id: types.number,
    name: types.string,
  },
}

Standard fragments

Use the fragment helper to create GraphQL Fragment, and spread the result into places the fragment is used.

query {
  user: user(id: 1) {
    ...userFragment
  }
  maleUsers: users(sex: MALE) {
    ...userFragment
  }
}

fragment userFragment on User {
  id
  name
  bankAccount {
    ...bankAccountFragment
  }
}

fragment bankAccountFragment on BankAccount {
  id
  branch
}
import { alias, fragment, query } from 'typed-graphqlify'

const bankAccountFragment = fragment('bankAccountFragment', 'BankAccount', {
  id: types.number,
  branch: types.string,
})

const userFragment = fragment('userFragment', 'User', {
  id: types.number,
  name: types.string,
  bankAccount: {
    ...bankAccountFragment,
  },
})

query({
  [alias('user', 'user(id: 1)')], {
    ...userFragment,
  },
  [alias('maleUsers', 'users(sex: MALE)')], {
    ...userFragment,
  },
}

Inline Fragment

Use on helper to write inline fragments.

query getHeroForEpisode {
  hero {
    id
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
import { on, query, types } from 'typed-graphqlify'

query('getHeroForEpisode', {
  hero: {
    id: types.number,
    ...on('Droid', {
      primaryFunction: types.string,
    }),
    ...on('Human', {
      height: types.number,
    }),
  },
})

If you are using a discriminated union pattern, then you can use the onUnion helper, which will automatically generate the union type for you:

query getHeroForEpisode {
  hero {
    id
    ... on Droid {
      kind
      primaryFunction
    }
    ... on Human {
      kind
      height
    }
  }
}
import { onUnion, query, types } from 'typed-graphqlify'

query('getHeroForEpisode', {
  hero: {
    id: types.number,
    ...onUnion({
      Droid: {
        kind: types.constant('Droid'),
        primaryFunction: types.string,
      },
      Human: {
        kind: types.constant('Human'),
        height: types.number,
      },
    }),
  },
})

This function will return a type of A | B, meaning that you can use the following logic to differentiate between the 2 types:

const droidOrHuman = queryResult.hero
if (droidOrHuman.kind === 'Droid') {
  const droid = droidOrHuman
  // ... handle droid
} else if (droidOrHument.kind === 'Human') {
  const human = droidOrHuman
  // ... handle human
}

Directive

Directive is not supported, but you can use alias to render it.

query {
  myState: myState @client
}
import { alias, query } from 'typed-graphqlify'

query({
  [alias('myState', 'myState @client')]: types.string,
})

See more examples at src/__tests__/index.test.ts

Usage with React Native

This library uses Symbol and Map, meaning that if you are targeting ES5 and lower, you will need to polyfill both of them.

So, you may need to import babel-polyfill in App.tsx.

import 'babel-polyfill'
import * as React from 'react'
import { View, Text } from 'react-native'
import { query, types } from 'typed-graphqlify'

const queryString = query({
  getUser: {
    user: {
      id: types.number,
    },
  },
})

export class App extends React.Component<{}> {
  render() {
    return (
      <View>
        <Text>{queryString}</Text>
      </View>
    )
  }
}

See: facebook/react-native#18932

Why not use apollo client:codegen?

There are some GraphQL -> TypeScript convertion tools. The most famous one is Apollo codegen:

https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output

In this section, we will go over why typed-graphqlify is a good alternative.

Disclaimer: I am not a heavy user of Apollo codegen, so the following points could be wrong. And I totally don't mean disrespect Apollo codegen.

Simplicity

Apollo codegen is a great tool. In addition to generating query interfaces, it does a lot of tasks including downloading schemas, schema validation, fragment spreading, etc.

However, great usability is the tradeoff of complexity.

There are some issues to generate interfaces with Apollo codegen.

I (and maybe everyone) don't know the exact reasons, but Apollo's codebase is too large to find out what the problem is.

On the other hand, typed-graphqlify is as simple as possible by design, and the logic is quite easy. If some issues happen, we can fix them easily.

Multiple Schemas problem

Currently Apollo codegen cannot handle multiple schemas.

Although I know this is a kind of edge case, but if we have the same type name on different schemas, which schema is used?

typed-graphqlify works even without schema

Some graphql frameworks, such as laravel-graphql, cannot print schema as far as I know. I agree that we should avoid to use such frameworks, but there must be situations that we cannot get graphql schema for some reasons.

Write GraphQL programmatically

It is useful to write GraphQL programmatically, although that is an edge case.

Imagine AWS management console:

image

If you build something like that with GraphQL, you have to build GraphQL dynamically and programmatically.

typed-graphqlify works for such cases without losing type information.

Contributing

To get started with a development installation of the typed-graphqlify, follow the instructions at our Contribution Guide.

Thanks

Inspired by

typed-graphqlify's People

Contributors

acro5piano avatar alewgbl avatar arjunyel avatar capaj avatar dependabot-preview[bot] avatar dependabot[bot] avatar gsa05 avatar kevinsimper avatar luvies avatar vkrol avatar yardwill avatar zzzen 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

typed-graphqlify's Issues

How to represent a union?

Hi, @acro5piano!

How to represent a type like string | null? I have tried types.oneOf([types.string, null] as const), but this gives me some wrong type in typescript v3.6.3:

Screen Shot 2019-10-08 at 19 47 11

Query custom scalar property

Would it be possible to add the ability to query custom scalar types? For example, the API I'm building uses the JSON type from graphql-type-json, and the client needs to query on this field.

I know what the type of this field will be (since the data come from elsewhere, so structure is a given), so something like

const queryObject = {
  getData: {
    data: types.custom<Array<Record<string, ...>>>(),
  },
}

Currently my workaround is to do:

export const extypes = {
  custom<T>(): T {
    return '' as any;
  },
};

and import it separately.

Request to add a runtime enum detection mechanism

First of all, thank you for maintaining such an excellent library! Clientside GraphQL in my application doesn't feel brittle anymore.

TL;DR - It would be nice to be able to detect whether a property is an enum at runtime.

I've created a wrapper around typed-graphqlify and graphql-code-generator that allows me to make type-safe queries tied to a generated GraphQL schema type map like this:

// can't use any string, Mutation must have a keyof 'signup'
const signup = Gql.mutation('signup')
  .setQuery({
    // properties must match DeepPartial<Mutation['signup']>
    id: Gql.string,
    email: Gql.string,
    username: Gql.string,
    role: Gql.optional.oneOf(UserRoles)!,
    confirmedAt: Gql.string,
  })
  // the developer is responsible for using the right input
  .setVariables<{ input: SignupInput }>({
    input: {
      email: Gql.string,
      password: Gql.string,
      username: Gql.string,
    },
  })
  .build();
  
console.log(signup.toString({ input: { ... } })); // query{...}

The code that achieves this is in $gql.ts

When a variable is an enum, I need to account for it at runtime so it doesn't get quoted by rawString. It's impossible to "correctly" distinguish a string enum from a string using inherent properties. However, I made a solution where the caller manually declares object paths to the enums. As I'm grapqhlifying variables, I track the path and skip quoting values that match the object paths. The resulting public API is very awkward:

Path.ts

const changeRoles = Gql.mutation('changeRoles')
  .setQuery([{
    id: Gql.string, role: Gql.optional.oneOf(UserRoles)!
  }])
  .setVariables<{ input: ChangeRolesInput }>({
    input: [{ userId: Gql.string, role: Gql.optional.oneOf(UserRoles)! }],
  })
  // in the variables, don't quote any objects matching object path: input[*].role
  .setEnumPaths([Path.create('input', Path.STAR, 'role')])
  .build();

This approach works, but it's error-prone and awkward. It would be nice to be able to detect enums at runtime readily. Since enums use the oneOf type definition, perhaps that method can attach extra metadata to demarcate enum fields.

How can I generate buildSchema when I'm using graphql

// import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { graphqlSync, buildSchema } from 'graphql'
import { query, types } from 'typed-graphqlify'

const schema = buildSchema(`
  type Query {
    name: String
  }
`)

const getMessageQuery = query({
  name: types.string,
})

export default class ExamplesController {
  public async index() {
    return graphqlSync({
      schema,
      source: getMessageQuery.toString(),
      rootValue: {
        name: () => 'Wednesday',
      },
    })
  }
}

Typing array elements

In the Readme.MD you wrote Note: Currently creating type from array element is not supported in TypeScript. See https://github.com/Microsoft/TypeScript/issues/28046

In that issue thread someone seems to have posted a solution that works with TS > 3.4. Does this help typed-graphqlify?

microsoft/TypeScript#28046 (comment)

Optional types not working?

Hi, when I define an optional type it doesn't seem to work? branch is defined as types.optional.string. Great idea for this library by the way.
Screenshot 2020-06-03 at 21 31 04

Proper way of creating scalar with parameters

First of all, thanks for creating this awesome library. My question is, how should I create a field that takes parameters, but returns a scalar instead of a nested object?

Example:

type Query {
  getContract: Contract
}

type Contract {
   title: String!
   isValid(timestamp: String!): Boolean!
}

Desired query:

query {
  getContract {
    title: String!
    isValid(timestamp: "2019-01-30")
  }
}

I can't use __params, because graphqlify will generate an empty { } block after the name and parameters.

My workaround:

const contractObj = {
  title: types.string,
  isValid: types.boolean, // for type inference and auto-compĺetion to work properly
}

delete contractObj.isValid;
contractObj['isValid(timestamp: "2019-01-30")'] = types.boolean;

const queryObj = { getContract: contractObj };
const queryString = graphqlify.query('MyQuery', queryObj);

I could have added ['isValid(timestamp: "2019-01-30")']: types.boolean to the object literal, but then auto-completion, type inference, etc, wouldn't work as desired.

Is something like this possible?

const contractObj = {
  title: types.string,
  isValid: withParameters(types.boolean, { timestamp: '"2018-01-01"' }),
};

API refactor

Currently we have to write the operation name like this:

graphqlify.query({
  getMotherAndFather: {
    mother: {
      id: types.number,
    },
    father: {
      id: types.number,
    },
  },
})

Now that #6 is merged even got better, but how about this?

graphqlify.query('getMotherAndFather', {
  mother: {
    id: types.number,
  },
  father: {
    id: types.number,
  },
})

in this way we can avoid using hackerly GraphQLData<queryObject> and less typing.

Quoting string values on mutations

Using your example mutation:

import { mutation, params } from 'typed-graphqlify'

mutation('updateUserMutation', {
  updateUser: params(
    {
      input: {
        name: 'Ben',
        slug: '/ben',
      },
    },
    {
      id: types.number,
      name: types.string,
    },
  ),
})

Yields the following query: mutation updateUserMutation{updateUser(input:{name:Ben,slug:/ben}){id name}}. Why doesn't the library wrap string values in double quotes? That's what my backend framework expects, so I suppose it's a standard?

String parameter bug

I found the following bug:

  it('render string params', () => {
    const queryObject = {
      user: {
        __params: { status: 'active' },
        id: types.number,
      },
    }
    const actual = graphqlify.query('getActiveUsers', queryObject)

    expect(actual).toEqual(gql`
      query getActiveUsers {
        user(status: "active") {
          id
        }
      }
    `)
  })

actual

user(status: active)

Strongly typed unions

When using { onUnion } from src/types.ts, I immediately found that it was error-prone and not inherently safe against schema changes. Instead of using that, I created a strongly typed union type that wraps onUnion. I would like to share and get your thoughts on adding it as a static method on { types } from src/types.ts which will supplement { onUnion } from src/types.ts.

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial<U>[] : T[P] extends object ? DeepPartial<T[P]> : T[P];
};

type Union<T extends string = string> = {
  __typename?: T;
};

type UnionSelection<U extends Union> = {
  [Typename in NonNullable<U['__typename']>]: DeepPartial<Omit<Extract<U, Union<Typename>>, '__typename'>>;
};

type Selection<S extends UnionSelection<any>, Typename extends keyof S> = S[Typename];

type Flattened<S extends UnionSelection<any>> = {
  [Typename in keyof S]: { __typename: Typename } & Selection<S, Typename>;
}[keyof S];

class types {
  // The reason why we return a function is to allow S to be inferred. Otherwise, we get the more genrealized
  // type and we have to specify all parameters, which would defeat the purpose of the convenience of not having
  // the specify a selection twice.
  static union = <U extends Union>() => <S extends UnionSelection<U>>(types: S): Flattened<S> => {
    const frags = {};
    for (const [__typename, fields] of Object.entries(types)) {
      Object.assign(frags, onUnion({ [__typename]: { __typename: t.constant(__typename), ...(fields as any) } }));
    }
    return frags as any;
  };
}

An example usage is:

type ConfirmEmailOutput = EmailConfirmation | NotFoundError | BadRequestError | ForbiddenError | UnknownError;

type EmailConfirmation = {
  __typename: 'EmailConfirmation';
  confirmedAt: string;
};

type NotFoundError = {
  __typename: 'NotFoundError';
  message: string;
};

type BadRequestError = {
  __typename: 'BadRequestError';
  message: string;
};

type ForbiddenError = {
  __typename: 'ForbiddenError';
  message: string;
};

type UnknownError = {
  __typename: 'UnknownError';
  message: string;
};

types.union<ConfirmEmailOutput>()({
  EmailConfirmation: {
    confirmedAt: types.string,
  },
  NotFoundError: {
    message: types.string,
  },
  BadRequestError: {
    message: types.string,
  },
  ForbiddenError: {
    message: types.string,
  },
  UnknownError: {
    message: types.string,
  },
});

By leveraging the __typename field, we have a discriminated union that allows union queries to be strongly typed:

image

It also requires all members of the union to be included, which prevents developers from forgetting to include a __typename from the union. In this example, I deleted the NotFoundError from the union selection object, but the compiler complains, which is the desired result. This enforces the invariant that all members of a union are required in the query:

image

What are your thoughts?

API refactor request-use object methods to call query/mutation

apollo client and many other clients have this kind of API:

graphqlify.mutation({...})
graphqlify.query({...})

and it feels nicer to write/read than

graphqlify('mutation', {...})
graphqlify('query', {

also saves one character.
So I know it's a little presumptuous to ask you to change it, but IMHO it would help people who are mostly used to apollo/urql up to speed faster.

If you'd agree I could even open a PR to address this.

[Question]: Error handling

Hi,

I was desperately searching for a typescript graphql client library that doesn’t rely on code generation, it this one could be a perfect match. However I do have some questions that are not covered by the README.

  • How does error handling work? There are different kind of errors (e.g. networking errors vs graphql errors). So how can I provide the schema/typings for graphql errors?
  • How to handle partial data, e.g. when there are some graphql errors for only a few fields but not all?
  • What happens if the data that is delivered by the server doesn’t match the expected schema? Is it possible to opt-in runtime validation, e.g. via zod?

.map files missing from the package causing warning in CRA 5.0.1 React apps

.map files are not part of the package.

When typed-graphqlify is added to a React app created with create-react-app v5.0.1, npm run start shows the following warning.

WARNING in ../../../node_modules/typed-graphqlify/dist/index.es.js
Module Warning (from ../../../node_modules/source-map-loader/dist/cjs.js):
Failed to parse source map from '/mnt/Data/1/Sources/GitHub/FooBar/node_modules/typed-graphqlify/dist/index.es.js.map' file: Error: ENOENT: no such file or directory, open '/mnt/Data/1/Sources/GitHub/FooBar/node_modules/typed-graphqlify/dist/index.es.js.map'

Array arguments in query params converting to objects

I'm trying to construct a graphQL query something like so:

import { params, query, types } from 'typed-graphqlify'

export const userTitles = (userIdsList) => ({
  id: types.string,
  user_titles: params(
   {},
    [{
      name: types.custom<string>(),
      category: {
        id: types.custom<string | number>(),
        name: types.custom<string>(),
        category_users: params(
          { user_ids: userIdsList },
          [{
            category_association_date: types.custom<string>(),
          }]
        ),
      },
    }]
  ),
});

const queryString = query({
    user: params(
      { id: 15 },
      userTitles([15])
    ),
  });

fetch('https://myserver.com/graphql', { 
  method: 'POST',
  body: JSON.stringify({
    query: queryString,
    variables: {},
  })

I expect the request payload to be:

query {
  user(id:15) {
    id 
    user_titles {
      name 
      category {
        id 
        name 
        category_users(user_ids: [15]) {
          category_association_date
        }
      }
    }
  }
}

but instead get a:

query {
  user(id:15) {
    id 
    user_titles {
      name 
      category {
        id 
        name 
        category_users(user_ids: {0: 15}) {
          category_association_date
        }
      }
    }
  }
}

Is there a way to convert the arguments to an array instead of an object as we're seeing above?

[Question] how to use variables

Hello, thanks for your library, looks pretty cool.

The doc says:

Currently typed-graphqlify can convert these GraphQL features:

  • Inputs
    • Variables
    • Param

But I did not find anything in doc or test on the use of variables.

It is possible to add an example ?

Error: Object cannot have no fields at renderObject

I am trying to implement a hasura mutation which looks like this:

mutation insertInviteMutation($objects: [invites_insert_input!]!, $on_conflict: invites_on_conflict) {
	insert_invites(object: $objects, on_conflict: $on_conflict) {
		id
		created_at
	}
}

My typescript code looks like this.

import { mutation, params, types} from 'typed-graphqlify';

const insetInviteMutation = mutation('myMutation ($objects: [invites_insert_input!]!, $on_conflict: invites_on_conflict)', {
	insert_invites: params(
		{
			objects: '$objects',
			on_conflict: '$on_conflict'
		},
		{
			id: types.string,
			created_at: types.string,
		}
	)
}).toString();

// I have even tried doing it through nested params

const nestedInsertInviteMutation = mutation('myMutation', params(
		{
			"$objects": "[invites_insert_input!]!",
			"$on_conflict": "invites_on_conflict"
		},
		{
			insert_invites: params(
				{
				objects: '$objects',
				on_conflict: '$on_conflict'
				},
				{
					id: types.string,
					created_at: types.string,
				}
			)
		}
	)
).toString();

But I got the error that Error: Object cannot have no fields at renderObject.
I have debugged it and its at this line . I can also see the test cases for these scenario. But for some reasons I am getting the error. You have added these test case after this I guess

I am running the latest version 3.0.2.
Please guide me what I am doing wrong here. Thank You.

how to do inline fragments?

on our Graphql api we use interfaces. To query an interface we need to make inline fragments such as:

query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}

if this lib can support these we can start putting it to production at https://www.looop.co/

Verifying a queryObject and have typeof getAbout.data the expected type?

Hello ! Thank you for your work!!

I would like to do

/* ../../types.d.ts */
type About = {
  id: number;
  title: string;
  count: number;
  not_queried: string;
};

import type * as GqlTypes from "../../types.d.ts";

const getAbout = query<GqlTypes.Query>("GetAbout", {
  about_by_pk: params(
    { id: 1 },
    { title: types.string, invalid_field: types.string, count: types.boolean },
  ),
});

Where invalid_field and count would be incorrect. And have typeof getAbout.data have the partial About type hence without not_queried.

Right now, by doing so, getAbout.data will have GqlTypes.Query type.

Do you have any idea ?

how to query __params field?

using __params to pass graphql parameters to a field resolver is nice, but what if I have an API which has __params field? How can I tell the typed-graphqlify that the __params is a field-not a params object?

I know it's not very likely, but it could happen.

Render fragment to string

It looks there is no way to turn a fragment into a string.

This will make it easier to incorporate into larger projects where fragments are used as a way for splitting up the app into smaller pieces.

It will benefit that you don't have to refactor everything up the chain if you want to add it to a leaf component.

const userProfileFragment = fragment("UserProfile", "User", {
  name: types.string,
});
toString(userProfileFragment)

Params of type array is not formatted correctly in Mutation.

I am trying to create a bulk insert mutation with parameter of type array For example

const mutationObject = mutation('HasuraBulkInsertUsers', {
  insert_users: params({
    objects: [ { name: "test"}, { name: "test2"} ]
  }, {
    returning: { id: types.number }
  })
})

my graphql server support objects to be an array. so using typed-graphiql I am generating this mutation like this

const bulkInsertQuery = { 
	inserts_users: params(
		[
			{
				name: "test"
			}, 
			{
				name: "test2"
			} 
		], 
		{
			returning: {
				id: type.number
		}
	})
};
const graphqlQuery = mutation(bulkInsertQuery);

this output query is something like this

mutation{insert_users: (object: {0: {name: "test"}, 1: {name: "test2"} } ) };

Is there a way to keep the array as it is without converting it into key values?

Any help would be appreciated. Thank you

Support inline fragment unions

I've been writing both a GraphQL server and consumer, and I've written a pattern of using discriminatory unions multiple times. Unfortunately, the way fragments work currently, it just makes each field optional, making it a bit weird to handle these kinds of unions properly. Ideally, you would want something that could mimic the following:

interface ObjA {
  name: 'a';
  prop: string;
}

interface ObjB {
  name: 'b';
  field: number;
}

type Union = ObjA | ObjB;

So far, this is the implementation I'm using, however, it's a little odd, so would need discussion before actually being implemented here:

export function onUnion<T extends Array<{}>>(typeNames: string[], fields: T): T extends Array<infer TElement> ? TElement : never {
  if (typeNames.length !== fields. length) {
    throw new Error('Type name and field arrays must be the same length');
  }

  let obj: any = {};

  for (let i = 0; i < fields.length; i++) {
    obj = {
      ...obj,
      ...on(typeNames[i], fields[i]),
    };
  }

  return obj;
}

Unfortunately, I have to make it accept 2 lists, one with type names and one with field objects, since otherwise TS didn't infer the types properly.

Using this function looks like so:

const query = {
  ...onUnion(
    [
      'ObjA',
      'ObjB',
    ],
    [
      {
        name: types.constant('a'),
        prop: types.string,
      },
      {
        name: types.constant('b'),
        field: types.number,
      },
    ],
  ),
};

types.custom incorrect type

Currently types.custom<T>() has the type (() => T) | undefined, where it should be () => (T | undefined). It is likely due to the Partial<typeof types> usage.

v3 roadmap

I think typed-graphqlify could do more than the current type alias.

While I was working on Apollo and Apollo Codegen instead of typed-graphqlify, I feel it inconvenient in the following points:

  • Importing a lot of code from __generated__ directories
  • Multiple schemas, in terms of microservices
  • No serializer concept

And typed-graphqlify lacks the following features:

  • Params are not type-safed (#52)
  • More elegant syntax (#67, #68)
  • Validate schema according to the actual schema

And I would like to add a feature, requesting and type inference from query.

Currently we have to write

import { query, types } from 'typed-graphqlify'

const query = {
  users: [{
    id: types.number,
    name: types.string,
  }]
}

export type ReturnTypeOfQuery = typeof query

export const usersQuery = query('GetUsers', query)

// in other file
import { usersQuery, ReturnTypeOfQuery } from './path/to/query'

const res: ReturnTypeOfQuery = (await apolloClient.query(usersQuery)).data

This is bothering, so if we can request like this:

import graphql from 'typed-graphqlify'

graphql.init({ baseUri: '/graphql' })

const res = await graphql({
  query: {
    users: [{
      id: types.number,
      name: types.string,
    }]
  },
  variables: {
    email: '@gmai.com',
  },
})

// and `typeof res` is like this:
interface res {
  users: {
    id: number,
    name: string
  }[],
}

This feature looks too much for this library, so I'm thinking splitting into other library though...

Did you miss some features?

Directives

query Block ($abc: Boolean, $page: Int) {
  id @include(if: $abc)
  list (page: $page) @skip(if: $abc) {
    id
    name @include(if: $abc)
  }
}

fragment userFragment on User {
  title @include(if: $abc)
}

And more, I will find out later. Good luck.

Using a scalar in an array outputs incorrect GraphQL

Given the following query:

import { graphqlify, types } from 'typed-graphqlify';

console.log(graphqlify.query({
  hero: {
    names: [types.string]
  }
}))

typed-graphqlify will output this:

query { hero { names {  } } }

When it should be this:

query { hero { names } }

How to remove query from the output query ?

In my app, all of my graphql query is live query, so i have to skip query and subscription from the query itself.

That means, instead of using

query {
users {
id
}
}

i use

{
users {
id
}
}

This is, i think possible to skip the operation name query, mutation, subscription by adding an optional parameter, so that the output string query doesn't contain those names.

graphql-tag integration

I would like to keep this library thin, but every time writing

import gql from 'graphql-tag'

const getUser = graphqlify.query({ getUser: ... })

const Compoent = graphql(gql(getUser))({ data }: Props) => ...

is annoying, so it would be great to have gql compatible mode.

If we implement it, the code should be like this:

// in bootstrap
import { useGraphQLTag } from 'typed-graphqlify'
useGraphQLTag(true)

// in Component.ts
const getUser = graphqlify.query({ getUser: ... })

const Compoent = graphql(getUser)({ data }: Props) => ...

Use Template Literal Types in TS 4.1

In the following repository, it parses SQL into TypeScript type. No codegen tool is needed anymore.

https://github.com/codemix/ts-sql

The syntax should be like this:

// Query.ts

import { QueryFactory } from 'typed-graphqlify'

const schema = gql`
  type Query { 
    hello: String!
  }
`

export type Query<T> = QueryFactory<T, typeof schema>
// HelloQuery.ts

import { Query } from './Query'

const HelloQuery = gql`
  { hello }
`

type IHelloQuery = Query<typeof query>

// Equivalent to the following. Cool!
//
// type IHelloQuery = {
//   data?: { 
//     hello: string
//   }
// }

I think we can do the same thing in GrapQL.

I've started to work on this, will use next branch.

How can params be used in sub trees of a query?

Suppose I have a gql query that looks like this:

"query category($id: Int!, $pageSize: Int!, $currentPage: Int!) {
  category(id: $id) {
    id
    description
    name
  }
  products(pageSize: $pageSize, currentPage: $currentPage) {
    items {
      id
      name
    }
  }
}
"

Is there any way I could write it using this library? I am able to do so for the individual category and product queries, but I wasn't able to do this for the full example, where params int he query are passed down in different trees.

Support standard fragments

Currently, if you want to do fragments, you have to do something like this:

import { graphqlify, types, alias } from 'typed-graphqlify';

const fragment = {
  name: types.string,
  appearsIn: types.string,
  friends:[
    {
      name: types.string
    }
  ]
}

const gql = {
  [alias('leftComparison', 'hero')]: {
    __params: {
      episode: 'EMPIRE'
    },
    ...fragment
  },
  [alias('rightComparison', 'hero')]: {
    __params: {
      episode: 'JEDI'
    },
    ...fragment
  }
}

console.log(graphqlify.query(gql));

which will output:

query {
  leftComparison: hero(episode: EMPIRE) {
    name
    appearsIn
    friends {
      name
    }
  }
  rightComparison: hero(episode: JEDI) {
    name
    appearsIn
    friends {
      name
    }
  }
}

This works, but is rather inefficient, since we know what bits are fragments beforehand, so the optimal code to output would be:

query {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}

The savings aren't hugely apparent here, but when you start building up large queries (which I have), you find that large parts of the GQL is duplicated and redundant.

I imagine a function that returns an object that would work like

const comparisonFields = fragment('comparisonFields', 'Character', {
  name: types.string,
  appearsIn: types.string,
  friends:[
    {
      name: types.string
    }
  ]
})

const gql = {
  [alias('leftComparison', 'hero')]: {
    __params: {
      episode: 'EMPIRE'
    },
    ...comparisonFields
  },
  [alias('rightComparison', 'hero')]: {
    __params: {
      episode: 'JEDI'
    },
    ...comparisonFields
  }
}

would work best.

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.