Skip to content
This repository has been archived by the owner on May 10, 2018. It is now read-only.

Using binding-ts for defining resolvers #72

Open
danielkcz opened this issue Mar 17, 2018 · 8 comments
Open

Using binding-ts for defining resolvers #72

danielkcz opened this issue Mar 17, 2018 · 8 comments

Comments

@danielkcz
Copy link

danielkcz commented Mar 17, 2018

I am not entirely sure what is the use case for binding-ts generator. I assume it's not meant to add type safety to resolvers? Either way, if I would like to make another generator for this case, would you accept PR for it or should it go as a standalone module? Unfortunately, it would be a lot of copy & paste.


Let me explain what issues there are for that generator to be used for resolvers properly. Besides regular types, enums, inputs ... which are correctly generated, there is this type.

export type Mutation = {
  authPlayer: (args: { input?: AuthPlayerInput }, context: { [key: string]: any }, info?: GraphQLResolveInfo | string) => Promise<AuthPlayer>
}

I cannot figure out how I would use that for typing my resolver. Also, there is an issue of a missing first argument being parent object.

Instead, I've figured out that this approach might serve much better.

export namespace Mutation {
  export authPlayer: (parent: any, args: { input?: AuthPlayerInput }, context: { [key: string]: any }, info?: GraphQLResolveInfo | string) => Promise<AuthPlayer>
}

const authPlayer: Mutation.authPlayer = async (_, { input }, ctx: Context) => {
   // nice type checking even for a return value
}

Looking at the code it shouldn't be that hard to do required tweaks, but it will be a lot of copy&paste which I am not for fond of :) Opinions?

@danielkcz
Copy link
Author

danielkcz commented Mar 18, 2018

Alright, I've managed to create my own generator and it's working nicely. I've also tweaked enum generation to have a real TS enum instead of a union. I am not even generating root schema interface as I don't see a reason for that. It's rather crude and hard-coded, mostly because I could not figure how to add this to .graphqlconfig correctly, but it works :)

import * as path from 'path'
import * as fs from 'fs-extra'
import { GraphQLObjectType, isNonNullType } from 'graphql'
import { generateCode } from 'graphql-static-binding'
import {
  generator as gcgenerator,
  renderFieldName,
  renderFieldType,
} from 'graphql-static-binding/dist/generators/graphcool-ts'
import { Generator } from 'graphql-static-binding/dist/types.d'

import { schemaFilePath, srcPath } from './paths'

export async function generateSchema() {
  const schema = await fs.readFile(schemaFilePath, 'utf8')
  const generator: Generator = {
    ...gcgenerator,
    Main: renderMainMethod,
    RootType: renderRootType,
    Header: renderHeader,
    SubscriptionType: renderSubscriptionType,
    SchemaType: renderSchemaInterface,
    GraphQLEnumType: renderEnumType,
  }
  const outPath = path.join(srcPath, 'generated', 'schema.ts')
  await fs.writeFile(outPath, generateCode(schema, generator), 'utf8')
  console.log(`Generated typings for schema to ${outPath}`)
}

function renderHeader(): string {
  return `// autogenerated from schema.graphql by running yarn gql:gen
import { GraphQLResolveInfo } from "graphql"\n
import { Context } from "../utils"
`
}

function renderMainMethod() {
  return ``
}

function renderEnumType(type) {
  const fieldDefinition = type
    .getValues()
    .map(function(e) {
      return `  ${e.name} = '${e.name}'`
    })
    .join(',\n')
  return `${renderDescription(type.description)}export enum ${
    type.name
  } { \n${fieldDefinition}\n}`
}

function renderRootType(type: GraphQLObjectType): string {
  const fieldDefinition = Object.keys(type.getFields())
    .map(f => {
      const field = type.getFields()[f]
      return `  export type ${field.name} = (parent: any, args: {${
        field.args.length > 0 ? ' ' : ''
      }${field.args
        .map(f => `${renderFieldName(f)}: ${renderFieldType(f.type)}`)
        .join(', ')}${
        field.args.length > 0 ? ' ' : ''
      }}, context: Context, info?: GraphQLResolveInfo | string) => Promise<${renderFieldType(
        field.type,
      )}${!isNonNullType(field.type) ? ' | null' : ''}>`
    })
    .join('\n')
  return `${renderDescription(type.description)}export namespace ${
    type.name
  } {\n${fieldDefinition}\n}`
}

function renderSubscriptionType(type: GraphQLObjectType): string {
  const fieldDefinition = Object.keys(type.getFields())
    .map(f => {
      const field = type.getFields()[f]
      return `  export type ${field.name} = (args: {${
        field.args.length > 0 ? ' ' : ''
      }${field.args
        .map(f => `${renderFieldName(f)}: ${renderFieldType(f.type)}`)
        .join(', ')}${
        field.args.length > 0 ? ' ' : ''
      }}, context: Context, infoOrQuery?: GraphQLResolveInfo | string) => Promise<AsyncIterator<${renderFieldType(
        field.type,
      )}>>`
    })
    .join('\n')

  return `${renderDescription(type.description)}export namespace ${
    type.name
  } {\n${fieldDefinition}\n}`
}

function renderSchemaInterface() {
  return ''
}

function renderDescription(description) {
  return (
    '' +
    (description
      ? '/*\n' +
        description.split('\n').map(function(l) {
          return ' * ' + l + '\n'
        }) +
        '\n */\n'
      : '')
  )
}

@jvbianchi
Copy link

Could you create a PR with a this new generator?

@danielkcz
Copy link
Author

Well, that's what I was asking at the top of my first comment. I would like to know an opinion from maintainers before going extra lengths of making unwanted PR.

Besides, current graphql-cli is not really ready for custom generators from what I've seen so far.

@jvbianchi
Copy link

@schickling and @kbrandwijk what do you think?

@schickling
Copy link
Collaborator

Thanks a lot for bringing this up @FredyC @jvbianchi. This is directly related to #65 and should be a lot easier to tackle once this is resolved. In the meanwhile a PR & adding it directly here seems like the best solution.

So just to reiterate: What you're planning to do is generating (typed) resolvers (not bindings) based on a provided GraphQL schema?

@danielkcz
Copy link
Author

danielkcz commented Mar 18, 2018

@schickling Exactly, it's not really a binding, that's why I wasn't sure if it fits within this repo or if it should go separately. The code above based on the following schema generates typings as below which makes it much easier to write code for resolvers and get type errors in case the schema changes.

type AuthPlayer {
  playerId: ID!
  token: String!
}

input AuthPlayerInput {
  name: String!
}

type Mutation {
  authPlayer(input: AuthPlayerInput!): AuthPlayer!
}

type Query {
  dummy: Boolean
}

type Subscription {
  dummy: Boolean
}
import { GraphQLResolveInfo } from "graphql"
import { Context } from "../utils"

export interface AuthPlayerInput {
  name: String
}
export interface AuthPlayer {
  playerId: ID_Output
  token: String
}
export type ID_Input = string | number
export type ID_Output = string
export type String = string
export type Boolean = boolean

export namespace Query {
  export type dummy = (parent: any, args: {}, context: Context, info?: GraphQLResolveInfo | string) => Promise<Boolean | null>
}

export namespace Mutation {
  export type authPlayer = (parent: any, args: { input: AuthPlayerInput }, context: Context, info?: GraphQLResolveInfo | string) => Promise<AuthPlayer>
}

export namespace Subscription {
  export type dummy = (parent: any, args: {}, context: Context, infoOrQuery?: GraphQLResolveInfo | string) => Promise<AsyncIterator<Boolean>>
}

Besides I don't think that current binding-ts is correct given it's missing parent arg there, but that's a different issue.

@schickling
Copy link
Collaborator

You're right, it's not really a GraphQL binding and we should restructure the project hierarchy accordingly. Until this has happened, I'd kindly ask you to use a fork in the meanwhile. This should be possible by using the resolutions feature.

I'd like to pick this up again in 1-2 weeks as we've been working on something similar.

@danielkcz
Copy link
Author

danielkcz commented Mar 18, 2018

There is a certainly a handful of utility functions within this package that made this generator of mine possible without too much hassle. That's something to consider as well to have these functions separated so it can be reused out of a binding generation concerns.

@jvbianchi feel free to do a fork with that code and make it more universal. I don't have a capacity for it right now.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants