UNPKG

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