Skip to content

Commit 93f5773

Browse files
authored
Merge pull request #68 from leapfrogtechnology/make-migration
Introduce make command to generate migration files
2 parents 2e81e04 + 68be9ca commit 93f5773

File tree

14 files changed

+367
-8
lines changed

14 files changed

+367
-8
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE {{table}};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
--
2+
-- Create {{table}} table.
3+
--
4+
CREATE TABLE {{table}} (
5+
id INT PRIMARY KEY
6+
);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"files": [
99
"/bin/run",
1010
"/bin/run.cmd",
11-
"/lib"
11+
"/lib",
12+
"/assets"
1213
],
1314
"bin": {
1415
"sync-db": "./bin/run"

src/commands/make.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { loadConfig } from '../config';
2+
import { printLine } from '../util/io';
3+
import { Command, flags } from '@oclif/command';
4+
import Configuration from '../domain/Configuration';
5+
import * as fileMakerService from '../service/fileMaker';
6+
7+
class Make extends Command {
8+
static description = 'Make migration files from the template.';
9+
10+
static args = [{ name: 'name', description: 'Object or filename to generate.', required: true }];
11+
static flags = {
12+
type: flags.string({
13+
char: 't',
14+
helpValue: 'TYPE',
15+
description: 'Type of file to generate.',
16+
default: 'migration',
17+
options: ['migration', 'view', 'procedure', 'function']
18+
})
19+
};
20+
21+
/**
22+
* CLI command execution handler.
23+
*
24+
* @returns {Promise<void>}
25+
*/
26+
async run(): Promise<void> {
27+
const { args, flags: parsedFlags } = this.parse(Make);
28+
const config = await loadConfig();
29+
const list = await this.makeFiles(config, args.name, parsedFlags.type);
30+
31+
for (const filename of list) {
32+
await printLine(`Created ${filename}`);
33+
}
34+
}
35+
36+
/**
37+
* Make files based on the given name and type.
38+
*
39+
* @param {Configuration} config
40+
* @param {string} name
41+
* @param {string} [type]
42+
* @returns {Promise<string[]>}
43+
*/
44+
async makeFiles(config: Configuration, name: string, type?: string): Promise<string[]> {
45+
switch (type) {
46+
case 'migration':
47+
return fileMakerService.makeMigration(config, name);
48+
49+
default:
50+
throw new Error(`Unsupported file type ${type}.`);
51+
}
52+
}
53+
}
54+
55+
export default Make;

src/migration/service/knexMigrator.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,7 @@ export async function resolveMigrationContext(
8080

8181
log(`Initialize migration context [sourceType=${config.migration.sourceType}]`);
8282

83-
const { basePath, migration } = config;
84-
85-
// Migration directory could be absolute OR could be relative to the basePath.
86-
const migrationPath = path.isAbsolute(migration.directory)
87-
? migration.directory
88-
: path.join(basePath, migration.directory);
83+
const migrationPath = getMigrationPath(config);
8984

9085
switch (config.migration.sourceType) {
9186
case 'sql':
@@ -101,3 +96,19 @@ export async function resolveMigrationContext(
10196
throw new Error(`Unsupported migration.sourceType value "${config.migration.sourceType}".`);
10297
}
10398
}
99+
100+
/**
101+
* Get Migration directory path.
102+
*
103+
* @param {Configuration} config
104+
* @returns {string}
105+
*/
106+
export function getMigrationPath(config: Configuration): string {
107+
const { basePath, migration } = config;
108+
// Migration directory could be absolute OR could be relative to the basePath.
109+
const migrationPath = path.isAbsolute(migration.directory)
110+
? migration.directory
111+
: path.join(basePath, migration.directory);
112+
113+
return migrationPath;
114+
}

src/service/fileMaker.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as path from 'path';
2+
3+
import * as fs from '../util/fs';
4+
import { log } from '../util/logger';
5+
import { interpolate } from '../util/string';
6+
import { getTimestampString } from '../util/ts';
7+
import Configuration from '../domain/Configuration';
8+
import { getMigrationPath } from '../migration/service/knexMigrator';
9+
10+
const MIGRATION_TEMPLATE_PATH = path.resolve(__dirname, '../../assets/templates/migration');
11+
const CREATE_TABLE_CONVENTION = /create_(\w+)_table/;
12+
13+
/**
14+
* Generate migration file(s).
15+
*
16+
* @param {string} filename
17+
* @returns {Promise<string[]>}
18+
*/
19+
export async function makeMigration(config: Configuration, filename: string): Promise<string[]> {
20+
if (config.migration.sourceType !== 'sql') {
21+
// TODO: We'll need to support different types of migrations eg both sql & js
22+
// For instance migrations in JS would have different context like JavaScriptMigrationContext.
23+
throw new Error(`Unsupported migration.sourceType value "${config.migration.sourceType}".`);
24+
}
25+
26+
let createUpTemplate = '';
27+
let createDownTemplate = '';
28+
29+
const migrationPath = getMigrationPath(config);
30+
const migrationPathExists = await fs.exists(migrationPath);
31+
32+
if (!migrationPathExists) {
33+
log(`Migration path does not exist, creating ${migrationPath}`);
34+
35+
await fs.mkdir(migrationPath, { recursive: true });
36+
}
37+
38+
const timestamp = getTimestampString();
39+
const upFilename = path.join(migrationPath, `${timestamp}_${filename}.up.sql`);
40+
const downFilename = path.join(migrationPath, `${timestamp}_${filename}.down.sql`);
41+
42+
// Use the create migration template if the filename follows the pattern: create_<table>_table.sql
43+
const createTableMatched = filename.match(CREATE_TABLE_CONVENTION);
44+
45+
if (createTableMatched) {
46+
const table = createTableMatched[1];
47+
48+
log(`Create migration for table: ${table}`);
49+
50+
createUpTemplate = await fs
51+
.read(path.join(MIGRATION_TEMPLATE_PATH, 'create_up.sql'))
52+
.then(template => interpolate(template, { table }));
53+
createDownTemplate = await fs
54+
.read(path.join(MIGRATION_TEMPLATE_PATH, 'create_down.sql'))
55+
.then(template => interpolate(template, { table }));
56+
}
57+
58+
await fs.write(upFilename, createUpTemplate);
59+
await fs.write(downFilename, createDownTemplate);
60+
61+
return [upFilename, downFilename];
62+
}

src/util/fs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as os from 'os';
33
import * as path from 'path';
44
import { promisify } from 'util';
55

6+
export const mkdir = promisify(fs.mkdir);
7+
68
/**
79
* Create a temporary directory and return it's path.
810
*

src/util/string.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Interpolate a given template string by filling the placeholders with the params.
3+
*
4+
* Placeholder syntax:
5+
* {{name}}
6+
*
7+
* @example
8+
* interpolate('<div>{{text}}</div>', {text: 'Hello World!'})
9+
* => '<div>Hello World!</div>'
10+
*
11+
* @param {string} template
12+
* @param {*} [params={}]
13+
* @returns {string}
14+
*/
15+
export function interpolate(template: string, params: any = {}): string {
16+
if (!params || !Object.keys(params)) {
17+
return template;
18+
}
19+
20+
let result = template;
21+
22+
for (const [key, value] of Object.entries(params)) {
23+
if (value === null || value === undefined) {
24+
continue;
25+
}
26+
27+
result = result.replace(new RegExp('{{' + key + '}}', 'g'), `${value}`);
28+
}
29+
30+
return result;
31+
}

src/util/ts.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,36 @@ export function getElapsedTime(timeStart: [number, number], fixed: false | numbe
1717

1818
return Number(timeElapsed.toFixed(fixed));
1919
}
20+
21+
/**
22+
* Gets a timestamp string for the given date.
23+
*
24+
* @param {Date} [date=new Date()]
25+
* @returns {string}
26+
*/
27+
export function getTimestampString(date: Date = new Date()): string {
28+
const dtf = new Intl.DateTimeFormat('en', {
29+
year: 'numeric',
30+
month: '2-digit',
31+
day: '2-digit',
32+
hour: '2-digit',
33+
minute: '2-digit',
34+
second: '2-digit'
35+
});
36+
37+
const [
38+
{ value: month },
39+
,
40+
{ value: day },
41+
,
42+
{ value: year },
43+
,
44+
{ value: hour },
45+
,
46+
{ value: minute },
47+
,
48+
{ value: second }
49+
] = dtf.formatToParts(date);
50+
51+
return `${year}${month}${day}${hour}${minute}${second}`;
52+
}

test/cli/commands/common.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const packageJson = fs.readFileSync(path.join(__dirname, '../../../package.json'
1111
const { version } = JSON.parse(packageJson.toString());
1212

1313
describe('CLI:', () => {
14-
describe('default run', () => {
14+
describe('with no args', () => {
1515
it('should display the usage information.', async () => {
1616
const { stdout } = await runCli([], { cwd });
1717

0 commit comments

Comments
 (0)