Skip to content

Instantly share code, notes, and snippets.

@tgriesser
Created December 6, 2019 19:31
Show Gist options
  • Save tgriesser/6dbd5cae8e80dbf5fe0d7270cdacda7f to your computer and use it in GitHub Desktop.
Save tgriesser/6dbd5cae8e80dbf5fe0d7270cdacda7f to your computer and use it in GitHub Desktop.
Codegen for GraphQL Tests
overwrite: true
hooks:
afterAllFileWrite:
- prettier --write
schema: 'packages/graphql-schema/api-schema.graphql'
generates:
packages/api-graphql/test/testgen/GeneratedGraphQLTestFns.gen.ts:
documents: 'packages/api-graphql/test/**/*.graphql'
plugins:
- add: "// This file is auto-generated, do not edit directly!\n/* eslint-disable */"
- typescript
- typescript-operations
- '@packages/custom-codegen-scripts/dist/codegenPluginTestFns'
import { CodegenPlugin } from '@graphql-codegen/plugin-helpers'
import {
ClientSideBaseVisitor,
LoadedFragment,
RawClientSideBasePluginConfig,
} from '@graphql-codegen/visitor-plugin-common'
import { camel, titleCase } from 'change-case'
import fs from 'fs'
import {
concatAST,
DocumentNode,
FragmentDefinitionNode,
Kind,
OperationDefinitionNode,
visit,
} from 'graphql'
import path from 'path'
type ExpressConfig = {
path: string
importName: string
importAlias?: string
}
interface GraphQLTestGenConfig extends RawClientSideBasePluginConfig {
express?: ExpressConfig
endpoint?: string
}
class GraphQLTestGenVisitor extends ClientSideBaseVisitor<
GraphQLTestGenConfig
> {
protected queryNames: string[] = []
protected mutationNames: string[] = []
protected operationMap = new Map<
string,
{
methodName: string
testFnName: string
operationName: string
variablesName: string
}
>()
constructor(
fragments: LoadedFragment[],
protected cfg: GraphQLTestGenConfig
) {
super(fragments, cfg, {})
}
OperationDefinition(node: OperationDefinitionNode) {
if (!node.name) {
throw new Error('Cannot have anonymous queries')
}
const operationName: string = this.convertName(node.name.value, {
suffix: titleCase(node.operation),
transformUnderscore: true,
useTypesPrefix: false,
})
const variablesName: string = this.convertName(node.name.value, {
suffix: titleCase(`${node.operation}Variables`),
useTypesPrefix: false,
})
const testFnName = camel(operationName)
if (node.operation === 'query') {
this.queryNames.push(node.name.value)
}
if (node.operation === 'mutation') {
this.mutationNames.push(node.name.value)
}
this.operationMap.set(node.name.value, {
testFnName,
operationName,
methodName: camel(node.name.value.replace(/^Test_/, '')),
variablesName,
})
return super.OperationDefinition(node)
}
getImports() {
const toPrint: string[] = []
toPrint.push(
`const ENDPOINT = ${JSON.stringify(this.cfg.endpoint || '/graphql')}`
)
if (this.cfg.express) {
const { importAlias = 'TestApp', importName, path } = this.cfg.express
const importSymbol = `${importName} as ${importAlias}`
toPrint.push(`import { InitialCreationData } from '@packages/test-utils'`)
toPrint.push(`import { ${importSymbol} } from ${JSON.stringify(path)}`)
}
const testFnBody = fs.readFileSync(
path.join(__dirname, '../tmpl-testFnBody.ts'),
'utf8'
)
toPrint.push(
testFnBody,
...super.getImports(),
`
class GQLSchemaOp {
constructor(protected initialData?: Pick<InitialCreationData, 'session'>) {}
protected _makeConfig(config: TestCaseConfigOptions): TestCaseConfigOptions {
if (this.initialData && this.initialData.session) {
return {session: this.initialData.session.cookie, ...config}
}
return config;
}
}
class GQLSchemaTestQuery extends GQLSchemaOp {
${this.operationMethod(this.queryNames.sort())}
}
class GQLSchemaTestMutation extends GQLSchemaOp {
${this.operationMethod(this.mutationNames.sort())}
}
export function gqlSchemaTests(initialData?: Pick<InitialCreationData, 'session'>) {
return {
get query() {
return new GQLSchemaTestQuery(initialData)
},
get mutation() {
return new GQLSchemaTestMutation(initialData)
}
}
}
`
)
return [toPrint.join('\n')]
}
protected operationMethod(operationNames: string[]) {
return operationNames
.map((name) => {
const def = this.operationMap.get(name)
if (!def) {
throw new Error(`Missing ${name}`)
}
return `${def.methodName}(variables: ${def.variablesName}, config: TestCaseConfigOptions = {}) {
return ${def.testFnName}(variables, this._makeConfig(config))
}`
})
.join('\n')
}
buildOperation = (
node: OperationDefinitionNode,
documentVariableName: string,
operationType: string,
operationResultType: string,
operationVariablesTypes: string
): string => {
if (!node.name) {
throw new Error('Cannot have an un-named GraphQL operation')
}
const operationName: string = this.convertName(node.name.value, {
suffix: titleCase(operationType),
transformUnderscore: true,
useTypesPrefix: false,
})
const testFnName = camel(operationName)
return [
'// DO NOT EDIT THIS FILE',
`export const ${testFnName} = makeGraphqlTestCase<${operationResultType}, ${operationVariablesTypes}>(${documentVariableName});`,
].join('\n')
}
}
export = {
plugin: (schema, documents, config) => {
const allAst = concatAST(
documents.reduce(
(prev, v) => {
return [...prev, v.content]
},
[] as DocumentNode[]
)
)
const operationsCount = allAst.definitions.filter(
(d) => d.kind === Kind.OPERATION_DEFINITION
)
if (operationsCount.length === 0) {
return ''
}
const allFragments = allAst.definitions.filter(
(d) => d.kind === Kind.FRAGMENT_DEFINITION
) as FragmentDefinitionNode[]
const loadedFragements = allFragments.map((fragmentDef) => ({
node: fragmentDef,
name: fragmentDef.name.value,
onType: fragmentDef.typeCondition.name.value,
isExternal: false,
}))
const visitor = new GraphQLTestGenVisitor(loadedFragements, config) as any
const visitorResult = visit(allAst, { leave: visitor }) as DocumentNode
return [
visitor.getImports(),
visitor.fragments,
...visitorResult.definitions.filter((t) => typeof t === 'string'),
].join('\n')
},
} as CodegenPlugin<RawClientSideBasePluginConfig>
/* eslint-disable */
import supertest from 'supertest'
import {
DocumentNode,
OperationDefinitionNode,
print,
GraphQLFormattedError,
} from 'graphql'
type GraphQLTestCaseResult<ResultType> = {
status: number
body: {
data: ResultType
errors?: GraphQLFormattedError[]
}
}
interface TestCaseConfigOptions {
throwOnError?: boolean
session?: string
}
let srv
beforeAll(() => {
// @ts-ignore
srv = supertest(TestApp)
})
/// ---------
// IMPORTANT!!!
// if you want to make changes to this file,
// do it in `graphql-test-codegen/tmpl/testFnBody.ts` rather than
// in the generated file
// ----------
// @ts-ignore - this file is fs.readFile'd and consumed internally so we don't export it
function makeGraphqlTestCase<ResultType, Variables>(document: DocumentNode) {
return async (
variables: Variables,
config: TestCaseConfigOptions = {}
): Promise<GraphQLTestCaseResult<ResultType>> => {
// @ts-ignore - this file is fs.readFile'd and consumed internally so this is already declared
const operationNode = document.definitions[0] as OperationDefinitionNode
const { throwOnError = true } = config
const body = {
query: print(document),
variables,
operationName: operationNode.name ? operationNode.name.value : null,
}
// @ts-ignore - defined elsewhere
let testReq = srv
// @ts-ignore - this file is fs.readFile'd and consumed internally so this is already declared
.post(ENDPOINT)
.send(body)
.set('Accept', 'application/json')
if (config.session) {
testReq = testReq.set('Cookie', [config.session])
}
return testReq
.catch((e) => {
if (e.response) {
console.error(e.response.text)
} else {
console.error(e)
}
throw e
})
.then((res) => {
const result = {
status: res.status,
body: res.body,
}
if (throwOnError && res.body.errors && res.body.errors.length) {
throw res.body.errors[0]
}
return result
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment