UNPKG

12.2 kBJavaScriptView Raw
1#!/usr/bin/env node
2/* eslint no-console:0, no-var:0 */
3const Liftoff = require('liftoff');
4const interpret = require('interpret');
5const path = require('path');
6const tildify = require('tildify');
7const commander = require('commander');
8const color = require('colorette');
9const argv = require('getopts')(process.argv.slice(2));
10const cliPkg = require('../package');
11const {
12 mkConfigObj,
13 resolveEnvironmentConfig,
14 exit,
15 success,
16 checkLocalModule,
17 getMigrationExtension,
18 getSeedExtension,
19 getStubPath,
20} = require('./utils/cli-config-utils');
21const { readFile, writeFile } = require('./../lib/util/fs');
22
23const { listMigrations } = require('./utils/migrationsLister');
24
25async function openKnexfile(configPath) {
26 const importFile = require('../lib/util/import-file'); // require me late!
27 let config = await importFile(configPath);
28 if (config && config.default) {
29 config = config.default;
30 }
31 if (typeof config === 'function') {
32 config = await config();
33 }
34 return config;
35}
36
37async function initKnex(env, opts) {
38 checkLocalModule(env);
39 if (process.cwd() !== env.cwd) {
40 process.chdir(env.cwd);
41 console.log(
42 'Working directory changed to',
43 color.magenta(tildify(env.cwd))
44 );
45 }
46
47 env.configuration = env.configPath
48 ? await openKnexfile(env.configPath)
49 : mkConfigObj(opts);
50
51 const resolvedConfig = resolveEnvironmentConfig(
52 opts,
53 env.configuration,
54 env.configPath
55 );
56 const knex = require(env.modulePath);
57 return knex(resolvedConfig);
58}
59
60function invoke(env) {
61 env.modulePath = env.modulePath || env.knexpath || process.env.KNEX_PATH;
62
63 const filetypes = ['js', 'coffee', 'ts', 'eg', 'ls'];
64
65 const cliVersion = [
66 color.blue('Knex CLI version:'),
67 color.green(cliPkg.version),
68 ].join(' ');
69
70 const localVersion = [
71 color.blue('Knex Local version:'),
72 color.green(env.modulePackage.version || 'None'),
73 ].join(' ');
74
75 commander
76 .version(`${cliVersion}\n${localVersion}`)
77 .option('--debug', 'Run with debugging.')
78 .option('--knexfile [path]', 'Specify the knexfile path.')
79 .option('--knexpath [path]', 'Specify the path to knex instance.')
80 .option('--cwd [path]', 'Specify the working directory.')
81 .option('--client [name]', 'Set DB client without a knexfile.')
82 .option('--connection [address]', 'Set DB connection without a knexfile.')
83 .option(
84 '--migrations-directory [path]',
85 'Set migrations directory without a knexfile.'
86 )
87 .option(
88 '--migrations-table-name [path]',
89 'Set migrations table name without a knexfile.'
90 )
91 .option(
92 '--env [name]',
93 'environment, default: process.env.NODE_ENV || development'
94 )
95 .option('--esm', 'Enable ESM interop.')
96 .option('--specific [path]', 'Specify one seed file to execute.');
97
98 commander
99 .command('init')
100 .description(' Create a fresh knexfile.')
101 .option(
102 `-x [${filetypes.join('|')}]`,
103 'Specify the knexfile extension (default js)'
104 )
105 .action(() => {
106 const type = (argv.x || 'js').toLowerCase();
107 if (filetypes.indexOf(type) === -1) {
108 exit(`Invalid filetype specified: ${type}`);
109 }
110 if (env.configuration) {
111 exit(`Error: ${env.knexfile} already exists`);
112 }
113 checkLocalModule(env);
114 const stubPath = `./knexfile.${type}`;
115 readFile(
116 path.dirname(env.modulePath) +
117 '/lib/migrate/stub/knexfile-' +
118 type +
119 '.stub'
120 )
121 .then((code) => {
122 return writeFile(stubPath, code);
123 })
124 .then(() => {
125 success(color.green(`Created ${stubPath}`));
126 })
127 .catch(exit);
128 });
129
130 commander
131 .command('migrate:make <name>')
132 .description(' Create a named migration file.')
133 .option(
134 `-x [${filetypes.join('|')}]`,
135 'Specify the stub extension (default js)'
136 )
137 .option(
138 `--stub [<relative/path/from/knexfile>|<name>]`,
139 'Specify the migration stub to use. If using <name> the file must be located in config.migrations.directory'
140 )
141 .action(async (name) => {
142 const opts = commander.opts();
143 opts.client = opts.client || 'sqlite3'; // We don't really care about client when creating migrations
144 const instance = await initKnex(env, opts);
145 const ext = getMigrationExtension(env, opts);
146 const configOverrides = { extension: ext };
147
148 const stub = getStubPath('migrations', env, opts);
149 if (stub) {
150 configOverrides.stub = stub;
151 }
152
153 instance.migrate
154 .make(name, configOverrides)
155 .then((name) => {
156 success(color.green(`Created Migration: ${name}`));
157 })
158 .catch(exit);
159 });
160
161 commander
162 .command('migrate:latest')
163 .description(' Run all migrations that have not yet been run.')
164 .option('--verbose', 'verbose')
165 .action(async () => {
166 try {
167 const instance = await initKnex(env, commander.opts());
168 const [batchNo, log] = await instance.migrate.latest();
169 if (log.length === 0) {
170 success(color.cyan('Already up to date'));
171 }
172 success(
173 color.green(`Batch ${batchNo} run: ${log.length} migrations`) +
174 (argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
175 );
176 } catch (err) {
177 exit(err);
178 }
179 });
180
181 commander
182 .command('migrate:up [<name>]')
183 .description(
184 ' Run the next or the specified migration that has not yet been run.'
185 )
186 .action((name) => {
187 initKnex(env, commander.opts())
188 .then((instance) => instance.migrate.up({ name }))
189 .then(([batchNo, log]) => {
190 if (log.length === 0) {
191 success(color.cyan('Already up to date'));
192 }
193
194 success(
195 color.green(
196 `Batch ${batchNo} ran the following migrations:\n${log.join(
197 '\n'
198 )}`
199 )
200 );
201 })
202 .catch(exit);
203 });
204
205 commander
206 .command('migrate:rollback')
207 .description(' Rollback the last batch of migrations performed.')
208 .option('--all', 'rollback all completed migrations')
209 .option('--verbose', 'verbose')
210 .action((cmd) => {
211 const { all } = cmd;
212
213 initKnex(env, commander.opts())
214 .then((instance) => instance.migrate.rollback(null, all))
215 .then(([batchNo, log]) => {
216 if (log.length === 0) {
217 success(color.cyan('Already at the base migration'));
218 }
219 success(
220 color.green(
221 `Batch ${batchNo} rolled back: ${log.length} migrations`
222 ) + (argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
223 );
224 })
225 .catch(exit);
226 });
227
228 commander
229 .command('migrate:down [<name>]')
230 .description(
231 ' Undo the last or the specified migration that was already run.'
232 )
233 .action((name) => {
234 initKnex(env, commander.opts())
235 .then((instance) => instance.migrate.down({ name }))
236 .then(([batchNo, log]) => {
237 if (log.length === 0) {
238 success(color.cyan('Already at the base migration'));
239 }
240 success(
241 color.green(
242 `Batch ${batchNo} rolled back the following migrations:\n${log.join(
243 '\n'
244 )}`
245 )
246 );
247 })
248 .catch(exit);
249 });
250
251 commander
252 .command('migrate:currentVersion')
253 .description(' View the current version for the migration.')
254 .action(() => {
255 initKnex(env, commander.opts())
256 .then((instance) => instance.migrate.currentVersion())
257 .then((version) => {
258 success(color.green('Current Version: ') + color.blue(version));
259 })
260 .catch(exit);
261 });
262
263 commander
264 .command('migrate:list')
265 .alias('migrate:status')
266 .description(' List all migrations files with status.')
267 .action(() => {
268 initKnex(env, commander.opts())
269 .then((instance) => {
270 return instance.migrate.list();
271 })
272 .then(([completed, newMigrations]) => {
273 listMigrations(completed, newMigrations);
274 })
275 .catch(exit);
276 });
277
278 commander
279 .command('migrate:unlock')
280 .description(' Forcibly unlocks the migrations lock table.')
281 .action(() => {
282 initKnex(env, commander.opts())
283 .then((instance) => instance.migrate.forceFreeMigrationsLock())
284 .then(() => {
285 success(
286 color.green(`Succesfully unlocked the migrations lock table`)
287 );
288 })
289 .catch(exit);
290 });
291
292 commander
293 .command('seed:make <name>')
294 .description(' Create a named seed file.')
295 .option(
296 `-x [${filetypes.join('|')}]`,
297 'Specify the stub extension (default js)'
298 )
299 .option(
300 `--stub [<relative/path/from/knexfile>|<name>]`,
301 'Specify the seed stub to use. If using <name> the file must be located in config.seeds.directory'
302 )
303 .action(async (name) => {
304 const opts = commander.opts();
305 opts.client = opts.client || 'sqlite3'; // We don't really care about client when creating seeds
306 const instance = await initKnex(env, opts);
307 const ext = getSeedExtension(env, opts);
308 const configOverrides = { extension: ext };
309 const stub = getStubPath('seeds', env, opts);
310 if (stub) {
311 configOverrides.stub = stub;
312 }
313
314 instance.seed
315 .make(name, configOverrides)
316 .then((name) => {
317 success(color.green(`Created seed file: ${name}`));
318 })
319 .catch(exit);
320 });
321
322 commander
323 .command('seed:run')
324 .description(' Run seed files.')
325 .option('--verbose', 'verbose')
326 .option('--specific', 'run specific seed file')
327 .action(() => {
328 initKnex(env, commander.opts())
329 .then((instance) => instance.seed.run({ specific: argv.specific }))
330 .then(([log]) => {
331 if (log.length === 0) {
332 success(color.cyan('No seed files exist'));
333 }
334 success(
335 color.green(`Ran ${log.length} seed files`) +
336 (argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
337 );
338 })
339 .catch(exit);
340 });
341
342 if (!process.argv.slice(2).length) {
343 commander.outputHelp();
344 }
345
346 commander.parse(process.argv);
347}
348
349const cli = new Liftoff({
350 name: 'knex',
351 extensions: interpret.jsVariants,
352 v8flags: require('v8flags'),
353 moduleName: require('../package.json').name,
354});
355
356cli.on('require', function (name) {
357 console.log('Requiring external module', color.magenta(name));
358});
359
360cli.on('requireFail', function (name) {
361 console.log(color.red('Failed to load external module'), color.magenta(name));
362});
363
364// FYI: The handling for the `--cwd` and `--knexfile` arguments is a bit strange,
365// but we decided to retain the behavior for backwards-compatibility. In
366// particular: if `--knexfile` is a relative path, then it will be resolved
367// relative to `--cwd` instead of the shell's CWD.
368//
369// So, the easiest way to replicate this behavior is to have the CLI change
370// its CWD to `--cwd` immediately before initializing everything else. This
371// ensures that Liftoff will then resolve the path to `--knexfile` correctly.
372if (argv.cwd) {
373 process.chdir(argv.cwd);
374}
375// Initialize 'esm' before cli.launch
376if (argv.esm) {
377 // enable esm interop via 'esm' module
378 // eslint-disable-next-line no-global-assign
379 require = require('esm')(module);
380 // https://github.com/standard-things/esm/issues/868
381 const ext = require.extensions['.js'];
382 require.extensions['.js'] = (m, fileName) => {
383 try {
384 // default to the original extension
385 // this fails if target file parent is of type='module'
386 return ext(m, fileName);
387 } catch (err) {
388 if (err && err.code === 'ERR_REQUIRE_ESM') {
389 return m._compile(
390 require('fs').readFileSync(fileName, 'utf8'),
391 fileName
392 );
393 }
394 throw err;
395 }
396 };
397}
398
399cli.launch(
400 {
401 configPath: argv.knexfile,
402 require: argv.require,
403 completion: argv.completion,
404 },
405 invoke
406);