1 | import { print, concatAST, Kind, visit } from 'graphql';
|
2 | import { ClientSideBaseVisitor, indentMultiline } from '@graphql-codegen/visitor-plugin-common';
|
3 | import autoBind from 'auto-bind';
|
4 | import { extname } from 'path';
|
5 |
|
6 | const 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 |
|
16 | const getParserForType = (type) => {
|
17 | if (type.name.value === 'Float') {
|
18 | return 'input => Number(input)';
|
19 | }
|
20 | };
|
21 | const mapVariableTypeToOclifType = (type) => {
|
22 | if (type.name.value === 'Boolean') {
|
23 | return 'boolean';
|
24 | }
|
25 | else if (['Float', 'Int'].includes(type.name.value)) {
|
26 |
|
27 |
|
28 | return 'integer';
|
29 | }
|
30 | else {
|
31 | return 'string';
|
32 | }
|
33 | };
|
34 |
|
35 | const 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 |
|
54 | const omitOclifDirectives = (node) => {
|
55 | const directives = node.directives.filter(directive => directive.name.value !== 'oclif');
|
56 | return Object.assign({}, node, { directives });
|
57 | };
|
58 |
|
59 | class 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 |
|
66 |
|
67 |
|
68 |
|
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 |
|
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 |
|
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 |
|
96 | const directive = operation.node.directives.find(directive => directive.name.value === 'oclif');
|
97 |
|
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 |
|
120 | export 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 |
|
137 | const 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 | };
|
157 | const 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 |
|
163 | export { GraphQLRequestVisitor, plugin, validate };
|