vadistic/graphql-extra

Examples & appilcations

vadistic opened this issue ยท 2 comments

For each main are of the project there should be a simple example

  • code first graphql SDL
  • codegens
  • testing graphql stuff

Something like @GavinRay97 example is a good start ๐Ÿ‘
https://repl.it/@GavinRay97/AquamarineGoldFibonacci

I think there is feature partity with https://github.com/khaosdoctor/gotql - but better because it's producting AST instead of strings. Simple graphql-extra based client could make nice example

I can give a generic GQL Schema -> Typed Language converter in ~100 lines:

Types:

import {
  FieldDefinitionApi,
  EnumValueApi,
  InputValueApi,
} from 'graphql-extra'

/**
 * An Enum for GraphQL scalars
 * Used to compose ScalarMaps for language-specific types codegen
 */
export enum ScalarTypes {
  ID = 'ID',
  INT = 'Int',
  FLOAT = 'Float',
  STRING = 'String',
  BOOLEAN = 'Boolean',
}

/**
 * An interface that TypeConverters must implement for how to
 * map GraphQL scalars to their corresponding language types
 */
export type ScalarMap = {
  [key in ScalarTypes]: string
}

export type Fieldlike = FieldDefinitionApi | InputValueApi
export interface ITypeMap {
  types: {
    [key: string]: Fieldlike[]
  }
  enums: {
    [key: string]: EnumValueApi[]
  }
}

Schema Tools:

import { ITypeMap } from './types'
import {
  documentApi,
  objectTypeApi,
  FieldDefinitionApi,
  DocumentApi,
  inputTypeApi,
  enumValueApi,
  enumTypeApi,
} from 'graphql-extra'

/**
 * Takes a Document API object and builds a map of it's types and their fields
 */
export function buildTypeMap(document: DocumentApi): ITypeMap {
  let res: ITypeMap = {
    types: {},
    enums: {},
  }

  for (let [typeName, astNode] of document.typeMap) {
    switch (astNode.kind) {
      case 'InputObjectTypeDefinition':
        res['types'][typeName] = inputTypeApi(astNode).getFields()
        break
      case 'ObjectTypeDefinition':
        res['types'][typeName] = objectTypeApi(astNode).getFields()
        break
      case 'EnumTypeDefinition':
        res['enums'][typeName] = enumTypeApi(astNode).node.values.map(
          enumValueApi
        )
        break
    }
  }

  return res
}

/**
 * Checks if type string exists in ScalarMap
 */
export const isScalar = (type: string) => {
  return type.toUpperCase() in ScalarTypes
}

/**
 * Takes a Field from graphql-extra's FieldDefinitionApi or InputValueApi
 * and serializes it to extract and format the important information:
 * Name, Type, Nullability, and whether it's a list
 */
export const serialize = (field: Fieldlike) => ({
  name: field.getName(),
  required: field.isNonNullType(),
  list: field.isListType(),
  type: field.getTypename(),
})

GQL -> Typescript Converter:

import { ScalarTypes, Fieldlike, ITypeMap } from './types'
import { buildTypeMap } from './schemaTools'
import { html as template } from 'common-tags'
import { EnumValueApi } from 'graphql-extra'

const scalarMap = {
  [ScalarTypes.ID]: 'number',
  [ScalarTypes.INT]: 'number',
  [ScalarTypes.FLOAT]: 'number',
  [ScalarTypes.STRING]: 'string',
  [ScalarTypes.BOOLEAN]: 'boolean',
}

const baseTypes = template`
  type Maybe<T> = T | null
`

const fieldFormatter = (field: Fieldlike) => {
  let { name, required, list, type } = serialize(field)
  let T = isScalar(type) ? scalarMap[type] : type
  // string -> Maybe<string>
  if (!required) T = `Maybe<${T}>`
  // Maybe<string> -> Array<Maybe<string>>
  if (list) T = `Array<${T}>`
  // Array<Maybe<string>> -> Maybe<Array<Maybe<string>>>
  if (!required && list) T = `Maybe<${T}>`
  // username: string -> username?: string
  if (!required) name = `${name}?`
  return { name, type: T }
}

const tsTypeDef = (typeName: string, fields: Fieldlike[]): string => {
  const fieldDefs = fields
    .map(fieldFormatter)
    .map(({ name, type }) => `${name}: ${type}`)
    .join('\n')

  return template`
    type ${typeName} = {
      ${fieldDefs}
    }`
}

const typeMapToTSTypes = (typeMap: ITypeMap) =>
  Object.entries(typeMap.types)
    .map(([typeName, fields]) => tsTypeDef(typeName, fields))
    .join('\n\n')

const tsEnumDef = (typeName: string, fields: EnumValueApi[]): string => {
  const fieldDefs = fields
    .map((field) => `${field.getName()} = '${field.getName()}'`)
    .join(',\n')

  return template`
    enum ${typeName} {
      ${fieldDefs}
    }`
}

const typeMapToTSEnums = (typeMap: ITypeMap) =>
  Object.entries(typeMap.enums)
    .map(([typeName, fields]) => tsEnumDef(typeName, fields))
    .join('\n\n')

const typeMapToTypescript = (typeMap: ITypeMap) =>
  baseTypes +
  '\n\n' +
  typeMapToTSTypes(typeMap) +
  '\n\n' +
  typeMapToTSEnums(typeMap)

export const graphqlSchemaToTypescript = (schema: string) =>
  typeMapToTypescript(buildTypeMap(schema))

This same pattern holds for any language

GQL -> Go Converter:

import { ScalarTypes, Fieldlike, ITypeMap } from './types'
import { buildTypeMap, serialize, isScalar } from './schemaTools'
import { html as template } from 'common-tags'
import { EnumValueApi } from 'graphql-extra'

const scalarMap = {
  [ScalarTypes.ID]: `int`,
  [ScalarTypes.INT]: `int`,
  [ScalarTypes.FLOAT]: `float32`,
  [ScalarTypes.STRING]: `string`,
  [ScalarTypes.BOOLEAN]: `bool`,
}

const fieldFormatter = (field: Fieldlike) => {
  let { name, required, list, type } = serialize(field)
  let T = isScalar(type) ? scalarMap[type] : type
  if (!required) T = `*${T}`
  if (list) T = `[]${T}`
  return { name, type: T }
}
const goTypeDef = (typeName, fields: Fieldlike[]) => {
  const fieldDefs = fields
    .map(fieldFormatter)
    .map(({ name, type }) => `${name} ${type}`)
    .join('\n')

  return template`
    type ${typeName} struct {
        ${fieldDefs}
    }
  `
}

const typeMapToGoTypes = (typeMap: ITypeMap) =>
  Object.entries(typeMap.types)
    .map(([typeName, fields]) => goTypeDef(typeName, fields))
    .join('\n\n')

// type LeaveType string
// const(
//   AnnualLeave LeaveType = "AnnualLeave"
//   Sick = "Sick"
//   BankHoliday = "BankHoliday"
//   Other = "Other"
// )
const goEnumDef = (typeName, fields: EnumValueApi[]) => {
  const fieldDefs = fields
    .map((field, idx) =>
      idx == 0
        ? `${field.getName()} ${typeName} = "${field.getName()}"`
        : `${field.getName()} = "${field.getName()}"`
    )
    .join('\n')

  return template`
    type ${typeName} string

    const(
        ${fieldDefs}
    )
  `
}

const typeMapToGoEnums = (typeMap: ITypeMap) =>
  Object.entries(typeMap.enums)
    .map(([typeName, fields]) => goEnumDef(typeName, fields))
    .join('\n\n')

const typeMapToGo = (typeMap: ITypeMap) =>
  typeMapToGoTypes(typeMap) + '\n\n' + typeMapToGoEnums(typeMap)

export const graphqlSchemaToGo = (schema: string) =>
  typeMapToGo(buildTypeMap(schema))

Demo:

import { graphqlSchemaToGo } from './go'
import { graphqlSchemaToTypescript } from './typescript'

const schema = `
  type Mutation {
    InsertUserAction(user_info: UserInfo!): TokenOutput
  }

  enum SOME_ENUM {
    TYPE_A
    TYPE_B
    TYPE_C
  }

  input UserInfo {
    username: String!
    password: String!
    enum_field: SOME_ENUM!
    nullable_field: Float
    nullable_list: [Int]
  }

  type TokenOutput {
    accessToken: String!
  }
`

console.log(graphqlSchemaToTypescript(schema))
console.log(graphqlSchemaToGo(schema))

I have Java, Kotlin, JSDoc, and Python as well ๐Ÿ‘
(There may be better ways to use graphql-extra than I am here, would love input on that, I got most of this from trying to read the source code)

It would be cool to generate typedefs for query components (IE React/Vue + Apollo) now that the Query/Mutation and SelectionSet stuff is done. I think it may be possible to emulate most of what graphql-code-generator does in a compact amount of code that's very straightforward.