Created
December 6, 2019 19:31
-
-
Save tgriesser/6dbd5cae8e80dbf5fe0d7270cdacda7f to your computer and use it in GitHub Desktop.
Codegen for GraphQL Tests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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