UNPKG

6.67 kBJavaScriptView Raw
1import { print, concatAST, Kind, visit } from 'graphql';
2import { ClientSideBaseVisitor, indentMultiline } from '@graphql-codegen/visitor-plugin-common';
3import autoBind from 'auto-bind';
4import { extname } from 'path';
5
6const getFlagConfigForVariableDefinition = (definition) => {
7 const { list, required, innerType } = getInnerType(definition.type);
8 const oclifType = mapVariableTypeToOclifType(innerType);
9 const parser = getParserForType(innerType);
10 return `${definition.variable.name.value}: flags.${oclifType}({
11 multiple: ${list},
12 required: ${required},${parser ? `\n parse: ${parser}` : ''}
13})`;
14};
15// Supply a custom parser for oclif flag configuration
16const getParserForType = (type) => {
17 if (type.name.value === 'Float') {
18 return 'input => Number(input)';
19 }
20};
21const mapVariableTypeToOclifType = (type) => {
22 if (type.name.value === 'Boolean') {
23 return 'boolean';
24 }
25 else if (['Float', 'Int'].includes(type.name.value)) {
26 // A quirk of oclif is that "integer" allows for any `number`-typed response, and then
27 // we supply our own parsing function to make sure it's a float and not an integer
28 return 'integer';
29 }
30 else {
31 return 'string';
32 }
33};
34// Retrieve the inner type if nested within List and/or NonNull
35const getInnerType = (type) => {
36 const result = {
37 list: false,
38 required: false,
39 };
40 let _type = type;
41 while (_type.kind !== 'NamedType') {
42 if (_type.kind === 'ListType') {
43 result.list = true;
44 }
45 else if (_type.kind === 'NonNullType') {
46 result.required = true;
47 }
48 _type = _type.type;
49 }
50 result.innerType = _type;
51 return result;
52};
53// remove all @oclif directives from the document for transmission to the server
54const omitOclifDirectives = (node) => {
55 const directives = node.directives.filter(directive => directive.name.value !== 'oclif');
56 return Object.assign({}, node, { directives });
57};
58
59class GraphQLRequestVisitor extends ClientSideBaseVisitor {
60 constructor(schema, fragments, rawConfig, info) {
61 super(schema, fragments, rawConfig, {});
62 this._operationsToInclude = [];
63 this._info = info;
64 const { handlerPath = '../../handler' } = rawConfig;
65 // FIXME: This is taken in part from
66 // presets/near-operation-file/src/index.ts:139. How do I build a path relative to the outputFile in the same way?
67 // A plugin doesn't appear to have access to the same "options.baseOutputDir" that the preset does.
68 // const absClientPath = resolve(info.outputFile, join(options.baseOutputDir, options.presetConfig.baseTypesPath));
69 autoBind(this);
70 this._additionalImports.push(`import { Command, flags } from '@oclif/command'`);
71 this._additionalImports.push(`import handler from '${handlerPath}'`);
72 }
73 buildOperation(node, documentVariableName, operationType, operationResultType, operationVariablesTypes) {
74 this._operationsToInclude.push({
75 node,
76 documentVariableName,
77 operationType,
78 operationResultType,
79 operationVariablesTypes,
80 });
81 return null;
82 }
83 // Clean client-side content (ie directives) out of the GraphQL document prior to sending to the server
84 get definition() {
85 const operation = this._operationsToInclude[0];
86 const clientOperation = print(omitOclifDirectives(operation.node));
87 return `const ${operation.documentVariableName} = \`\n${clientOperation}\``;
88 }
89 // Generate the code required for this CLI operation
90 get cliContent() {
91 if (this._operationsToInclude.length !== 1) {
92 throw new Error(`Each graphql document should have exactly one operation; found ${this._operationsToInclude.length} while generating ${this._info.outputFile}.`);
93 }
94 const operation = this._operationsToInclude[0];
95 // Find the @oclif directive in the client document, if it's there
96 const directive = operation.node.directives.find(directive => directive.name.value === 'oclif');
97 // Remap the directive's fields ie @oclif(description: "a name") to a more usable format
98 const directiveValues = {};
99 if (directive) {
100 directiveValues.examples = [];
101 directive.arguments.forEach(arg => {
102 const value = 'value' in arg.value ? arg.value.value.toString() : null;
103 const { value: name } = arg.name;
104 if (name === 'description') {
105 directiveValues.description = value;
106 }
107 else if (name === 'example') {
108 directiveValues.examples.push(value);
109 }
110 else {
111 throw new Error(`Invalid field supplied to @oclif directive: ${name}`);
112 }
113 });
114 }
115 const { description, examples } = directiveValues;
116 const flags = operation.node.variableDefinitions.map(getFlagConfigForVariableDefinition);
117 return `
118${this.definition}
119
120export default class ${operation.node.name.value} extends Command {
121 ${description ? `\nstatic description = "${description}";\n` : ''}
122 ${examples ? `\nstatic examples: string[] = ${JSON.stringify(examples)};\n` : ''}
123 static flags = {
124 help: flags.help({ char: 'h' }),
125${indentMultiline(flags.join(',\n'), 2)}
126 };
127
128 async run() {
129 const { flags } = this.parse(${operation.node.name.value});
130 await handler({ command: this, query: ${operation.documentVariableName}, variables: flags });
131 }
132}
133`;
134 }
135}
136
137const plugin = (schema, documents, config, info) => {
138 const allAst = concatAST(documents.reduce((prev, v) => {
139 return [...prev, v.document];
140 }, []));
141 const allFragments = [
142 ...allAst.definitions.filter(d => d.kind === Kind.FRAGMENT_DEFINITION).map(fragmentDef => ({
143 node: fragmentDef,
144 name: fragmentDef.name.value,
145 onType: fragmentDef.typeCondition.name.value,
146 isExternal: false,
147 })),
148 ...(config.externalFragments || []),
149 ];
150 const visitor = new GraphQLRequestVisitor(schema, allFragments, config, info);
151 visit(allAst, { leave: visitor });
152 return {
153 prepend: visitor.getImports(),
154 content: visitor.cliContent,
155 };
156};
157const validate = async (schema, documents, config, outputFile) => {
158 if (extname(outputFile) !== '.ts') {
159 throw new Error(`Plugin "typescript-oclif" requires output file extensions to be ".ts"!`);
160 }
161};
162
163export { GraphQLRequestVisitor, plugin, validate };