UNPKG

10.1 kBPlain TextView Raw
1import { CliPlugin } from "@test-graphql-cli/common";
2import { CodeFileLoader } from '@graphql-toolkit/code-file-loader';
3import { GitLoader } from '@graphql-toolkit/git-loader';
4import { GithubLoader } from '@graphql-toolkit/github-loader';
5import { ApolloEngineLoader } from '@graphql-toolkit/apollo-engine-loader';
6import { PrismaLoader } from '@graphql-toolkit/prisma-loader';
7import { ensureFile } from 'fs-extra';
8import { writeFile as fsWriteFile } from 'fs';
9import { join } from 'path';
10import { graphQLInputContext, InputModelTypeContext, GraphbackCRUDGeneratorConfig } from "@graphback/core"
11import { migrateDB } from "graphql-migrations"
12import { createClient, ClientDocuments } from "@graphback/codegen-client"
13import { createResolvers, ResolverGeneratorOptions } from "@graphback/codegen-resolvers"
14import { createSchema, SchemaGeneratorOptions } from "@graphback/codegen-schema"
15import { GeneratedResolvers } from '@graphback/codegen-resolvers/types/api/resolverTypes';
16import Listr, { ListrTask } from 'listr';
17import { prompt } from 'inquirer';
18import chokidar from 'chokidar';
19import debounce from 'debounce';
20import { GraphQLExtensionDeclaration } from 'graphql-config';
21import { print } from 'graphql';
22
23export interface GenerateConfig {
24 folders: {
25 model: string;
26 schema: string;
27 resolvers: string;
28 client: string;
29 migrations: string;
30 };
31 graphqlCRUD: GraphbackCRUDGeneratorConfig;
32 generator: {
33 resolvers: ResolverGeneratorOptions
34 schema: SchemaGeneratorOptions
35 client: { format: 'ts' | 'gql' }
36 }
37 db: { database: string; dbConfig: any; };
38}
39
40const FormatExtensionMap = {
41 'ts': 'ts',
42 'js': 'js',
43 'gql': 'graphql',
44};
45
46export function writeFile(path: string, data: any) {
47 return new Promise<void>(async (resolve, reject) => {
48 await ensureFile(path);
49 fsWriteFile(path, data, err => {
50 if (err) {
51 reject(err);
52 }
53 resolve();
54 })
55 })
56}
57
58export function globPromise(glob: string, options: import('glob').IOptions = {}) {
59 return new Promise<string[]>(async (resolve, reject) => {
60 const { default: globAsync } = await import('glob');
61 globAsync(glob, options, (err, data) => {
62 if (err) {
63 reject(err);
64 }
65 resolve(data);
66 })
67 })
68}
69
70export async function createSchemaFile(cwd: string, generatedSchema: string, config: GenerateConfig) {
71 const extension = FormatExtensionMap[config.generator.schema.format];
72 return writeFile(join(cwd, config.folders.schema, 'generated.' + extension), generatedSchema);
73}
74
75export async function createResolversFiles(cwd: string, resolvers: GeneratedResolvers, config: GenerateConfig) {
76 const extension = FormatExtensionMap[config.generator.resolvers.format];
77 return Promise.all(
78 resolvers.types.map(typeResolver =>
79 writeFile(join(cwd, config.folders.resolvers, 'generated', typeResolver.name + '.' + extension), typeResolver.output)
80 )
81 );
82}
83
84export async function createBackendFiles(cwd: string, inputContext: InputModelTypeContext[], config: GenerateConfig) {
85 const resolvers = createResolvers(inputContext, config.generator.resolvers);
86 const schema = createSchema(inputContext, config.generator.schema);
87
88 await Promise.all([
89 createSchemaFile(cwd, schema, config),
90 createResolversFiles(cwd, resolvers, config)
91 ])
92}
93
94export async function createFragments(cwd: string, generated: ClientDocuments, config: GenerateConfig) {
95 const extension = FormatExtensionMap[config.generator.client.format];
96 return Promise.all(generated.fragments.map((fragment: any) => writeFile(
97 join(cwd, config.folders.client, 'generated', 'fragments', fragment.name + '.' + extension),
98 fragment.implementation,
99 )));
100}
101
102export async function createQueries(cwd: string, generated: ClientDocuments, config: GenerateConfig) {
103 const extension = FormatExtensionMap[config.generator.client.format];
104 return Promise.all(generated.queries.map(query => writeFile(
105 join(cwd, config.folders.client, 'generated', 'queries', query.name + '.' + extension),
106 query.implementation,
107 )));
108}
109
110export async function createMutations(cwd: string, generated: ClientDocuments, config: GenerateConfig) {
111 const extension = FormatExtensionMap[config.generator.client.format];
112 return Promise.all(generated.mutations.map(mutation => writeFile(
113 join(cwd, config.folders.client, 'generated', 'mutations', mutation.name + '.' + extension),
114 mutation.implementation,
115 )));
116}
117
118export async function createSubscriptions(cwd: string, generated: ClientDocuments, config: GenerateConfig) {
119 const extension = FormatExtensionMap[config.generator.client.format];
120 return Promise.all(generated.subscriptions.map(subscription => writeFile(
121 join(cwd, config.folders.client, 'generated', 'subscriptions', subscription.name + '.' + extension),
122 subscription.implementation,
123 )));
124}
125
126export async function createClientFiles(cwd: string, inputContext: InputModelTypeContext[], config: GenerateConfig) {
127 const generated = await createClient(inputContext, { output: config.generator.client.format });
128 await Promise.all([
129 createFragments(cwd, generated, config),
130 createQueries(cwd, generated, config),
131 createMutations(cwd, generated, config),
132 createSubscriptions(cwd, generated, config),
133 ]);
134}
135
136export async function createDatabaseMigration(schema: string, config: GenerateConfig) {
137 const dbConfig = {
138 client: config.db.database,
139 connection: config.db.dbConfig,
140 };
141
142 await migrateDB(dbConfig, schema);
143}
144
145interface CliFlags {
146 db: boolean, client: boolean, backend: boolean, silent: boolean, watch: boolean
147}
148
149export const runGeneration = async ({db, client, backend, silent }: CliFlags, cwd: string, generateConfig: GenerateConfig, schemaString: string) => {
150
151 const tasks: ListrTask[] = [];
152
153 if (backend || client) {
154 // Creates model context that is shared with all generators to provide results
155 const inputContext = graphQLInputContext.createModelContext(schemaString, generateConfig.graphqlCRUD)
156
157 if (backend) {
158 tasks.push({
159 title: 'Generating Backend Schema and Resolvers',
160 task: () => createBackendFiles(cwd, inputContext, generateConfig),
161 })
162 }
163 if (client) {
164 tasks.push({
165 title: 'Generating Client-side Operations',
166 task: () => createClientFiles(cwd, inputContext, generateConfig),
167 })
168 }
169 }
170
171 if (db) {
172 tasks.push({
173 title: 'Running Database Migration',
174 task: () => createDatabaseMigration(schemaString, generateConfig),
175 })
176 }
177
178 const listr = new Listr(tasks, {
179 renderer: silent ? 'silent' : 'default',
180 // it doesn't stop when one of tasks failed, to finish at least some of outputs
181 exitOnError: false,
182 // run 4 at once
183 concurrent: 4,
184 });
185
186 await listr.run();
187}
188
189const GenerateExtension: GraphQLExtensionDeclaration = api => {
190 // Schema
191 api.loaders.schema.register(new CodeFileLoader());
192 api.loaders.schema.register(new GitLoader());
193 api.loaders.schema.register(new GithubLoader());
194 api.loaders.schema.register(new ApolloEngineLoader());
195 api.loaders.schema.register(new PrismaLoader());
196
197 return {
198 name: 'generate'
199 };
200};
201
202export const plugin: CliPlugin = {
203 init({ program, loadProjectConfig, reportError }) {
204 program
205 .command('generate')
206 .option('--db')
207 .option('--client')
208 .option('--backend')
209 .option('--silent')
210 .option('-w, --watch', 'Watch for changes and execute generation automatically')
211 .action(async (cliFlags: CliFlags) => {
212 try {
213 const config = await loadProjectConfig({
214 extensions: [GenerateExtension]
215 });
216 const generateConfig: GenerateConfig = await config.extension('generate');
217
218 if (!generateConfig) {
219 throw new Error(`You should provide a valid 'generate' config to generate schema from data model`);
220 }
221
222 if (!generateConfig.folders) {
223 throw new Error(`'generate' config missing 'folders' section that is required`);
224 }
225
226 if (!cliFlags.db && !cliFlags.client && !cliFlags.backend) {
227 const { selections } = await prompt([
228 {
229 type: 'checkbox',
230 name: 'selections',
231 message: 'What do you want to generate?',
232 choices: [
233 {
234 value: 'backend',
235 name: 'Backend Schema and Resolvers',
236 },
237 {
238 value: 'client',
239 name: 'Client-Side Operation',
240 }, {
241 value: 'db',
242 name: 'Database Creation and Migration',
243 }
244 ]
245 }
246 ]);
247 cliFlags.db = selections.includes('db');
248 cliFlags.client = selections.includes('client');
249 cliFlags.backend = selections.includes('backend');
250
251 }
252
253 const debouncedExec = debounce(async () => {
254 try {
255 const schemaDocument = await config.loadSchema(join(config.dirpath, generateConfig.folders.model + '/**/*.graphql'), 'DocumentNode');
256 const schemaString = print(schemaDocument);
257 await runGeneration(cliFlags, config.dirpath, generateConfig, schemaString);
258 } catch(e) {
259 reportError(e);
260 }
261 console.info('Watching for changes...');
262 }, 100);
263
264 if (cliFlags.watch) {
265 chokidar.watch(generateConfig.folders.model, {
266 persistent: true,
267 cwd: config.dirpath,
268 }).on('all', debouncedExec);
269 } else {
270 const schemaDocument = await config.loadSchema(join(config.dirpath, generateConfig.folders.model + '/**/*.graphql'), 'DocumentNode');
271 const schemaString = print(schemaDocument);
272 await runGeneration(cliFlags, config.dirpath, generateConfig, schemaString);
273 process.exit(0);
274 }
275
276 } catch (e) {
277 reportError(e);
278 }
279 })
280 }
281}