@deterministic-code/migraters 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/cli.yaml +264 -0
- package/dist/bin/migrate-create.js +0 -0
- package/dist/bin/migrate-down.js +0 -0
- package/dist/bin/migrate-init.js +0 -0
- package/dist/bin/migrate-setup.js +0 -0
- package/dist/bin/migrate-up.js +0 -0
- package/package.json +7 -6
- package/src/abstractions/migrater.ts +0 -13
- package/src/abstractions/sql-dialect-base.ts +0 -36
- package/src/abstractions/sql-dialect-factory.ts +0 -29
- package/src/abstractions/sql-dialect.ts +0 -17
- package/src/bin/migrate-create.ts +0 -80
- package/src/bin/migrate-down.ts +0 -163
- package/src/bin/migrate-init.ts +0 -88
- package/src/bin/migrate-setup.ts +0 -98
- package/src/bin/migrate-up.ts +0 -111
- package/src/cli-contract.ts +0 -25
- package/src/cli-spec.ts +0 -64
- package/src/cli.ts +0 -52
- package/src/create-sequence.ts +0 -17
- package/src/datasource-migrate.ts +0 -314
- package/src/dialects/mysql-dialect.test.ts +0 -93
- package/src/dialects/mysql-dialect.ts +0 -67
- package/src/dialects/oracle-dialect.test.ts +0 -118
- package/src/dialects/oracle-dialect.ts +0 -91
- package/src/dialects/postgres-dialect.test.ts +0 -80
- package/src/dialects/postgres-dialect.ts +0 -65
- package/src/dialects/sqlite-dialect.rollback.test.ts +0 -30
- package/src/dialects/sqlite-dialect.test.ts +0 -77
- package/src/dialects/sqlite-dialect.ts +0 -96
- package/src/dialects/sqlserver-dialect.test.ts +0 -99
- package/src/dialects/sqlserver-dialect.ts +0 -87
- package/src/generate-help.test.ts +0 -33
- package/src/generate-help.ts +0 -173
- package/src/help.test.ts +0 -48
- package/src/help.ts +0 -12
- package/src/index.ts +0 -23
- package/src/infrastructure/connection-resolver.ts +0 -18
- package/src/infrastructure/default-factory.test.ts +0 -18
- package/src/infrastructure/default-factory.ts +0 -17
- package/src/infrastructure/interop-default.test.ts +0 -9
- package/src/infrastructure/interop-default.ts +0 -2
- package/src/infrastructure/message-templates.ts +0 -30
- package/src/infrastructure/split-on-go.ts +0 -19
- package/src/infrastructure/sql-templates.ts +0 -10
- package/src/infrastructure/templates-root.ts +0 -13
- package/src/init-select.test.ts +0 -26
- package/src/init-select.ts +0 -73
- package/src/migrater.ts +0 -92
- package/src/migration-client.ts +0 -21
- package/src/path-exists.ts +0 -10
- package/src/setup-sql.test.ts +0 -16
- package/src/sql.ts +0 -17
- package/tsconfig.build.json +0 -19
package/src/generate-help.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
-
import { parse } from 'yaml';
|
|
5
|
-
|
|
6
|
-
type HelpArg = {
|
|
7
|
-
flag: string;
|
|
8
|
-
placeholder?: string;
|
|
9
|
-
required: boolean;
|
|
10
|
-
help: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
type HelpCommand = {
|
|
14
|
-
verb: string;
|
|
15
|
-
summary: string;
|
|
16
|
-
examples: string;
|
|
17
|
-
show_connection_env?: boolean;
|
|
18
|
-
args: HelpArg[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type HelpSpec = {
|
|
22
|
-
providers: string[];
|
|
23
|
-
connection_environment: Record<string, string[]>;
|
|
24
|
-
commands: HelpCommand[];
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
28
|
-
const CLI_YAML = join(REPO_ROOT, 'cli.yaml');
|
|
29
|
-
const HELP_DIR = join(REPO_ROOT, 'shared', 'templates', 'help');
|
|
30
|
-
|
|
31
|
-
const requireString = (value: unknown, path: string): string => {
|
|
32
|
-
if (typeof value !== 'string' || value.trim() === '') {
|
|
33
|
-
throw new Error(`cli.yaml: ${path} must be a non-empty string`);
|
|
34
|
-
}
|
|
35
|
-
return value;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const parseHelpSpec = (raw: unknown): HelpSpec => {
|
|
39
|
-
if (raw === null || typeof raw !== 'object') {
|
|
40
|
-
throw new Error('cli.yaml: root must be a mapping');
|
|
41
|
-
}
|
|
42
|
-
const doc = raw as Record<string, unknown>;
|
|
43
|
-
if (!Array.isArray(doc.providers) || doc.providers.some((p) => typeof p !== 'string')) {
|
|
44
|
-
throw new Error('cli.yaml: providers must be a string list');
|
|
45
|
-
}
|
|
46
|
-
const envRaw = doc.connection_environment;
|
|
47
|
-
if (envRaw === null || typeof envRaw !== 'object' || Array.isArray(envRaw)) {
|
|
48
|
-
throw new Error('cli.yaml: connection_environment must be a mapping');
|
|
49
|
-
}
|
|
50
|
-
const connection_environment: Record<string, string[]> = {};
|
|
51
|
-
for (const [dialect, vars] of Object.entries(envRaw)) {
|
|
52
|
-
if (!Array.isArray(vars) || vars.some((v) => typeof v !== 'string')) {
|
|
53
|
-
throw new Error(`cli.yaml: connection_environment.${dialect} must be a string list`);
|
|
54
|
-
}
|
|
55
|
-
connection_environment[dialect] = vars;
|
|
56
|
-
}
|
|
57
|
-
if (!Array.isArray(doc.commands)) {
|
|
58
|
-
throw new Error('cli.yaml: commands must be a list');
|
|
59
|
-
}
|
|
60
|
-
const commands = doc.commands.map((item, i) => {
|
|
61
|
-
if (item === null || typeof item !== 'object') {
|
|
62
|
-
throw new Error(`cli.yaml: commands[${i}] must be a mapping`);
|
|
63
|
-
}
|
|
64
|
-
const row = item as Record<string, unknown>;
|
|
65
|
-
const verb = requireString(row.verb, `commands[${i}].verb`);
|
|
66
|
-
if (!Array.isArray(row.args)) {
|
|
67
|
-
throw new Error(`cli.yaml: commands.${verb}.args must be a list`);
|
|
68
|
-
}
|
|
69
|
-
return {
|
|
70
|
-
verb,
|
|
71
|
-
summary: requireString(row.summary, `commands.${verb}.summary`),
|
|
72
|
-
examples: requireString(row.examples, `commands.${verb}.examples`),
|
|
73
|
-
show_connection_env: row.show_connection_env === true,
|
|
74
|
-
args: row.args.map((arg, j) => {
|
|
75
|
-
if (arg === null || typeof arg !== 'object') {
|
|
76
|
-
throw new Error(`cli.yaml: commands.${verb}.args[${j}] must be a mapping`);
|
|
77
|
-
}
|
|
78
|
-
const a = arg as Record<string, unknown>;
|
|
79
|
-
return {
|
|
80
|
-
flag: requireString(a.flag, `commands.${verb}.args[${j}].flag`),
|
|
81
|
-
placeholder: typeof a.placeholder === 'string' ? a.placeholder : undefined,
|
|
82
|
-
required: a.required === true,
|
|
83
|
-
help: requireString(a.help, `commands.${verb}.args[${j}].help`),
|
|
84
|
-
};
|
|
85
|
-
}),
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
return {
|
|
89
|
-
providers: doc.providers as string[],
|
|
90
|
-
connection_environment,
|
|
91
|
-
commands,
|
|
92
|
-
};
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const wrapArgHelp = (flag: string, help: string, flagWidth: number): string => {
|
|
96
|
-
const lines = help.replace(/\s+$/, '').split('\n');
|
|
97
|
-
const indent = ` ${flag.padEnd(flagWidth)} `;
|
|
98
|
-
const cont = ' '.repeat(indent.length);
|
|
99
|
-
return [indent + lines[0], ...lines.slice(1).map((line) => cont + line.trimStart())].join('\n');
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const renderFlags = (args: HelpArg[]): string => {
|
|
103
|
-
const flagWidth = Math.max(...args.map((a) => a.flag.length));
|
|
104
|
-
return args.map((a) => wrapArgHelp(a.flag, a.help, flagWidth)).join('\n');
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const renderConnectionEnv = (spec: HelpSpec): string => {
|
|
108
|
-
const nameWidth = Math.max(...spec.providers.map((p) => p.length));
|
|
109
|
-
const lines = spec.providers.map((name) => {
|
|
110
|
-
const vars = spec.connection_environment[name];
|
|
111
|
-
if (!vars) {
|
|
112
|
-
throw new Error(`cli.yaml: connection_environment missing ${name}`);
|
|
113
|
-
}
|
|
114
|
-
return ` ${name.padEnd(nameWidth)} ${vars.join(', ')}`;
|
|
115
|
-
});
|
|
116
|
-
return [
|
|
117
|
-
'Per-dialect env vars consulted when --connection is omitted:',
|
|
118
|
-
...lines,
|
|
119
|
-
].join('\n');
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const indentBlock = (text: string): string =>
|
|
123
|
-
text
|
|
124
|
-
.split('\n')
|
|
125
|
-
.map((line) => (line.length === 0 ? '' : ` ${line}`))
|
|
126
|
-
.join('\n');
|
|
127
|
-
|
|
128
|
-
export const renderHelpTemplate = (spec: HelpSpec, command: HelpCommand): string => {
|
|
129
|
-
const parts = [
|
|
130
|
-
'{{usage}}',
|
|
131
|
-
'',
|
|
132
|
-
command.summary.replace(/\s+$/, ''),
|
|
133
|
-
'',
|
|
134
|
-
renderFlags(command.args),
|
|
135
|
-
'',
|
|
136
|
-
'Examples:',
|
|
137
|
-
indentBlock(command.examples.replace(/\s+$/, '')),
|
|
138
|
-
];
|
|
139
|
-
if (command.show_connection_env) {
|
|
140
|
-
parts.push('', renderConnectionEnv(spec));
|
|
141
|
-
}
|
|
142
|
-
return `${parts.join('\n')}\n`;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export const renderHelpTemplates = (raw: unknown): Record<string, string> => {
|
|
146
|
-
const spec = parseHelpSpec(raw);
|
|
147
|
-
return Object.fromEntries(spec.commands.map((c) => [c.verb, renderHelpTemplate(spec, c)]));
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
export const generateHelpFiles = async (): Promise<string[]> => {
|
|
151
|
-
const files = renderHelpTemplates(parse(await readFile(CLI_YAML, 'utf8')));
|
|
152
|
-
const written: string[] = [];
|
|
153
|
-
const packagedDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
154
|
-
const packagedCli = join(packagedDir, 'cli.yaml');
|
|
155
|
-
await writeFile(packagedCli, await readFile(CLI_YAML, 'utf8'));
|
|
156
|
-
written.push(packagedCli);
|
|
157
|
-
const packagedBundle = join(packagedDir, 'bundle.yaml');
|
|
158
|
-
await writeFile(packagedBundle, await readFile(join(REPO_ROOT, 'bundle.yaml'), 'utf8'));
|
|
159
|
-
written.push(packagedBundle);
|
|
160
|
-
for (const [verb, text] of Object.entries(files)) {
|
|
161
|
-
const path = join(HELP_DIR, `${verb}.txt`);
|
|
162
|
-
await writeFile(path, text);
|
|
163
|
-
written.push(path);
|
|
164
|
-
}
|
|
165
|
-
return written;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
169
|
-
const written = await generateHelpFiles();
|
|
170
|
-
for (const path of written) {
|
|
171
|
-
console.log(path);
|
|
172
|
-
}
|
|
173
|
-
}
|
package/src/help.test.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
fillHelpTemplate,
|
|
4
|
-
migrateCommand,
|
|
5
|
-
verbFromCommand,
|
|
6
|
-
type MigrateVerb,
|
|
7
|
-
} from './cli-contract.ts';
|
|
8
|
-
import { loadHelp, loadProgramUsage } from './help.ts';
|
|
9
|
-
|
|
10
|
-
const VERBS: MigrateVerb[] = ['setup', 'up', 'down', 'create'];
|
|
11
|
-
|
|
12
|
-
describe('unified help loader', () => {
|
|
13
|
-
it('maps migrate-<verb> onto the shared template file', () => {
|
|
14
|
-
expect(verbFromCommand('migrate-create')).toBe('create');
|
|
15
|
-
expect(migrateCommand('create')).toBe('migrate-create');
|
|
16
|
-
expect(() => verbFromCommand('MigrateRunner')).toThrow(/Unknown migrate command/);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('fills {{command}} and {{usage}} from cli.yaml', () => {
|
|
20
|
-
const filled = fillHelpTemplate('{{usage}}\n {{command}} --name x\n', 'migrate-create');
|
|
21
|
-
expect(filled.startsWith('Usage: migrate-create --provider')).toBe(true);
|
|
22
|
-
expect(filled).toContain('migrate-create --name x');
|
|
23
|
-
expect(filled).not.toContain('{{');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('loads program usage with the same migrate-<verb> Usage lines', async () => {
|
|
27
|
-
const text = await loadProgramUsage();
|
|
28
|
-
for (const verb of VERBS) {
|
|
29
|
-
expect(text).toContain(`Usage: ${migrateCommand(verb)} --provider`);
|
|
30
|
-
}
|
|
31
|
-
expect(text).toContain('Usage: migrate-init');
|
|
32
|
-
expect(text).not.toContain('{{command}}');
|
|
33
|
-
expect(text).not.toContain('MigrateRunner');
|
|
34
|
-
expect(text).not.toMatch(/^Usage: (create|up|down|setup) /m);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('loads the same filled help the rust and csharp runners produce', async () => {
|
|
38
|
-
for (const verb of VERBS) {
|
|
39
|
-
const command = migrateCommand(verb);
|
|
40
|
-
const text = await loadHelp(command);
|
|
41
|
-
expect(text.startsWith(`Usage: ${command} --provider`)).toBe(true);
|
|
42
|
-
expect(text).toContain(`${command} --provider sqlite`);
|
|
43
|
-
expect(text).not.toContain('{{command}}');
|
|
44
|
-
expect(text).not.toMatch(/^Usage: (create|up|down|setup) /m);
|
|
45
|
-
expect(text).not.toMatch(/^\s+(create|up|down|setup) --provider/m);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
});
|
package/src/help.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { fillHelpTemplate, programUsageText, verbFromCommand } from './cli-contract.ts';
|
|
4
|
-
import { templatesRoot } from './infrastructure/templates-root.ts';
|
|
5
|
-
|
|
6
|
-
export const loadHelp = async (command: string): Promise<string> => {
|
|
7
|
-
const verb = verbFromCommand(command);
|
|
8
|
-
const text = await readFile(join(templatesRoot(), 'help', `${verb}.txt`), 'utf8');
|
|
9
|
-
return fillHelpTemplate(text, command);
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const loadProgramUsage = async (): Promise<string> => programUsageText();
|
package/src/index.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export * from './datasource-migrate.ts';
|
|
2
|
-
export type {
|
|
3
|
-
CreateArg,
|
|
4
|
-
CreateResponse,
|
|
5
|
-
DownArg,
|
|
6
|
-
IMigrater,
|
|
7
|
-
SetupArg,
|
|
8
|
-
UpArg,
|
|
9
|
-
} from './abstractions/migrater.ts';
|
|
10
|
-
export { getMigrater, Migrater } from './migrater.ts';
|
|
11
|
-
export {
|
|
12
|
-
fillHelpTemplate,
|
|
13
|
-
migrateCommand,
|
|
14
|
-
verbFromCommand,
|
|
15
|
-
type MigrateVerb,
|
|
16
|
-
} from './cli-contract.ts';
|
|
17
|
-
export { loadHelp, loadProgramUsage } from './help.ts';
|
|
18
|
-
export {
|
|
19
|
-
fillTemplate,
|
|
20
|
-
loadMessage,
|
|
21
|
-
loadScaffold,
|
|
22
|
-
} from './infrastructure/message-templates.ts';
|
|
23
|
-
export { normalizeDialect, pathExists, type SqlDialect } from './sql.ts';
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { ISqlDialect } from '../abstractions/sql-dialect.ts';
|
|
2
|
-
|
|
3
|
-
export class ConnectionResolver {
|
|
4
|
-
constructor(private readonly env: NodeJS.ProcessEnv = process.env) {}
|
|
5
|
-
|
|
6
|
-
fromEnvironment(dialect: ISqlDialect): string | null {
|
|
7
|
-
for (const name of dialect.connectionEnvironmentVariables) {
|
|
8
|
-
const value = this.env[name];
|
|
9
|
-
if (value) return value;
|
|
10
|
-
}
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
resolve(dialect: ISqlDialect, fromFlag: string | null): string | null {
|
|
15
|
-
if (fromFlag) return fromFlag;
|
|
16
|
-
return this.fromEnvironment(dialect);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { defaultDialectFactory } from './default-factory.ts';
|
|
3
|
-
|
|
4
|
-
describe('defaultDialectFactory', () => {
|
|
5
|
-
it('registers every dialect name and alias', () => {
|
|
6
|
-
const names = defaultDialectFactory.all().map((d) => d.name);
|
|
7
|
-
expect(names).toEqual(['sqlite', 'postgres', 'mysql', 'sqlserver', 'oracle']);
|
|
8
|
-
expect(defaultDialectFactory.get('sqlite3').name).toBe('sqlite');
|
|
9
|
-
expect(defaultDialectFactory.get('postgresql').name).toBe('postgres');
|
|
10
|
-
expect(defaultDialectFactory.get('mariadb').name).toBe('mysql');
|
|
11
|
-
expect(defaultDialectFactory.get('mssql').name).toBe('sqlserver');
|
|
12
|
-
expect(defaultDialectFactory.get('mssqlserver').name).toBe('sqlserver');
|
|
13
|
-
expect(defaultDialectFactory.get('ms-sql-server').name).toBe('sqlserver');
|
|
14
|
-
expect(defaultDialectFactory.get('ora').name).toBe('oracle');
|
|
15
|
-
expect(defaultDialectFactory.tryGet('nope')).toBeUndefined();
|
|
16
|
-
expect(() => defaultDialectFactory.get('nope')).toThrow(/Unknown SQL dialect/);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { SqlDialectFactory } from '../abstractions/sql-dialect-factory.ts';
|
|
2
|
-
import { MysqlDialect } from '../dialects/mysql-dialect.ts';
|
|
3
|
-
import { OracleDialect } from '../dialects/oracle-dialect.ts';
|
|
4
|
-
import { PostgresDialect } from '../dialects/postgres-dialect.ts';
|
|
5
|
-
import { SqliteDialect } from '../dialects/sqlite-dialect.ts';
|
|
6
|
-
import { SqlServerDialect } from '../dialects/sqlserver-dialect.ts';
|
|
7
|
-
import { ConnectionResolver } from './connection-resolver.ts';
|
|
8
|
-
|
|
9
|
-
export const defaultDialectFactory = new SqlDialectFactory([
|
|
10
|
-
new SqliteDialect(),
|
|
11
|
-
new PostgresDialect(),
|
|
12
|
-
new MysqlDialect(),
|
|
13
|
-
new SqlServerDialect(),
|
|
14
|
-
new OracleDialect(),
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
export const connectionResolver = new ConnectionResolver();
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { interopDefault } from './interop-default.ts';
|
|
3
|
-
|
|
4
|
-
describe('interopDefault', () => {
|
|
5
|
-
it('prefers default when present and falls back to the module namespace', () => {
|
|
6
|
-
expect(interopDefault({ default: { connect: 1 } })).toEqual({ connect: 1 });
|
|
7
|
-
expect(interopDefault({ connect: 2 })).toEqual({ connect: 2 });
|
|
8
|
-
});
|
|
9
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { templatesRoot } from './templates-root.ts';
|
|
4
|
-
|
|
5
|
-
export const fillTemplate = (text: string, vars: Record<string, string>): string => {
|
|
6
|
-
let out = text;
|
|
7
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
8
|
-
out = out.replaceAll(`{{${key}}}`, value);
|
|
9
|
-
}
|
|
10
|
-
return out;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const loadMessage = async (
|
|
14
|
-
relativePath: string,
|
|
15
|
-
vars: Record<string, string> = {},
|
|
16
|
-
): Promise<string> => {
|
|
17
|
-
const text = await readFile(
|
|
18
|
-
join(templatesRoot(), 'messages', `${relativePath}.txt`),
|
|
19
|
-
'utf8',
|
|
20
|
-
);
|
|
21
|
-
return fillTemplate(text, vars);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const loadScaffold = async (
|
|
25
|
-
name: 'up' | 'down',
|
|
26
|
-
vars: Record<string, string>,
|
|
27
|
-
): Promise<string> => {
|
|
28
|
-
const text = await readFile(join(templatesRoot(), 'scaffold', `${name}.sql`), 'utf8');
|
|
29
|
-
return fillTemplate(text, vars);
|
|
30
|
-
};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const GO_LINE = /^\s*GO\s*$/i;
|
|
2
|
-
|
|
3
|
-
/** Split migration SQL on whole-line GO batch separators. */
|
|
4
|
-
export function splitOnGo(text: string): string[] {
|
|
5
|
-
const batches: string[] = [];
|
|
6
|
-
let buf = '';
|
|
7
|
-
for (const line of text.replace(/\r\n/g, '\n').split('\n')) {
|
|
8
|
-
if (GO_LINE.test(line)) {
|
|
9
|
-
const s = buf.trim();
|
|
10
|
-
if (s.length > 0) batches.push(s);
|
|
11
|
-
buf = '';
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
buf = buf.length > 0 ? `${buf}\n${line}` : line;
|
|
15
|
-
}
|
|
16
|
-
const tail = buf.trim();
|
|
17
|
-
if (tail.length > 0) batches.push(tail);
|
|
18
|
-
return batches;
|
|
19
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import type { SqlDialect } from '../sql.ts';
|
|
4
|
-
import { templatesRoot } from './templates-root.ts';
|
|
5
|
-
|
|
6
|
-
export const readSqlTemplate = async (
|
|
7
|
-
dialect: SqlDialect,
|
|
8
|
-
name: 'migrates' | 'migrate_logs',
|
|
9
|
-
): Promise<string> =>
|
|
10
|
-
(await readFile(join(templatesRoot(), 'sql', dialect, `${name}.sql`), 'utf8')).trim();
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { dirname, join } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
|
|
6
|
-
/** Repo `shared/templates`; after build, `dist/templates` (copied from shared). */
|
|
7
|
-
export const templatesRoot = (): string => {
|
|
8
|
-
const normalized = HERE.replaceAll('\\', '/');
|
|
9
|
-
if (normalized.endsWith('/dist/infrastructure')) {
|
|
10
|
-
return join(HERE, '..', 'templates');
|
|
11
|
-
}
|
|
12
|
-
return join(HERE, '..', '..', '..', 'shared', 'templates');
|
|
13
|
-
};
|
package/src/init-select.test.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { loadSelectedPackages, resolveInitDialects } from './init-select.ts';
|
|
3
|
-
|
|
4
|
-
describe('init dialect selection', () => {
|
|
5
|
-
it('accepts aliases and keeps cli.yaml order', () => {
|
|
6
|
-
expect(resolveInitDialects(['ora', 'pg'], false)).toEqual(['postgres', 'oracle']);
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('restores every provider for --all', () => {
|
|
10
|
-
expect(resolveInitDialects([], true)).toEqual([
|
|
11
|
-
'sqlite',
|
|
12
|
-
'postgres',
|
|
13
|
-
'mysql',
|
|
14
|
-
'sqlserver',
|
|
15
|
-
'oracle',
|
|
16
|
-
]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('reads typescript packages from bundle.yaml', async () => {
|
|
20
|
-
const selected = await loadSelectedPackages(['sqlite', 'postgres']);
|
|
21
|
-
expect(selected.packages.map((p) => p.name).sort()).toEqual(['better-sqlite3', 'pg']);
|
|
22
|
-
expect(selected.allDriverPackages).toEqual(
|
|
23
|
-
expect.arrayContaining(['better-sqlite3', 'pg', 'mysql2', 'mssql', 'oracledb']),
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
});
|
package/src/init-select.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { parse } from 'yaml';
|
|
6
|
-
import { cliSpec } from './cli-spec.ts';
|
|
7
|
-
|
|
8
|
-
export type SelectedDialects = {
|
|
9
|
-
names: string[];
|
|
10
|
-
packages: { name: string; version: string }[];
|
|
11
|
-
allDriverPackages: string[];
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const stripKey = (raw: string): string => raw.replace(/[\s_-]/g, '').toLowerCase();
|
|
15
|
-
|
|
16
|
-
export const normalizeInitProvider = (raw: string): string | undefined => {
|
|
17
|
-
const key = stripKey(raw);
|
|
18
|
-
const direct = cliSpec.providers.find((p) => stripKey(p) === key);
|
|
19
|
-
if (direct) return direct;
|
|
20
|
-
return cliSpec.provider_aliases[key];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const resolveInitDialects = (raw: string[], all: boolean): string[] => {
|
|
24
|
-
if (all) return [...cliSpec.providers];
|
|
25
|
-
if (raw.length === 0) {
|
|
26
|
-
throw new Error('pass --dialect <provider> at least once, or --all');
|
|
27
|
-
}
|
|
28
|
-
const selected = new Set<string>();
|
|
29
|
-
for (const value of raw) {
|
|
30
|
-
const name = normalizeInitProvider(value);
|
|
31
|
-
if (!name) {
|
|
32
|
-
throw new Error(`unsupported provider: ${value}`);
|
|
33
|
-
}
|
|
34
|
-
selected.add(name);
|
|
35
|
-
}
|
|
36
|
-
return cliSpec.providers.filter((name) => selected.has(name));
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type BundleDialect = {
|
|
40
|
-
packages?: { name: string; version: string }[];
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type BundleDoc = {
|
|
44
|
-
languages?: {
|
|
45
|
-
typescript?: {
|
|
46
|
-
dialects?: Record<string, BundleDialect>;
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const bundleYamlPath = (): string => {
|
|
52
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
53
|
-
const candidates = [
|
|
54
|
-
join(here, '..', 'bundle.yaml'),
|
|
55
|
-
join(here, '..', '..', 'bundle.yaml'),
|
|
56
|
-
join(process.cwd(), 'bundle.yaml'),
|
|
57
|
-
];
|
|
58
|
-
const found = candidates.find((candidate) => existsSync(candidate));
|
|
59
|
-
if (!found) {
|
|
60
|
-
throw new Error('bundle.yaml not found');
|
|
61
|
-
}
|
|
62
|
-
return found;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export const loadSelectedPackages = async (dialects: string[]): Promise<SelectedDialects> => {
|
|
66
|
-
const raw = parse(await readFile(bundleYamlPath(), 'utf8')) as BundleDoc;
|
|
67
|
-
const byName = raw.languages?.typescript?.dialects ?? {};
|
|
68
|
-
const allDriverPackages = [
|
|
69
|
-
...new Set(Object.values(byName).flatMap((d) => (d.packages ?? []).map((p) => p.name))),
|
|
70
|
-
];
|
|
71
|
-
const packages = dialects.flatMap((name) => byName[name]?.packages ?? []);
|
|
72
|
-
return { names: dialects, packages, allDriverPackages };
|
|
73
|
-
};
|
package/src/migrater.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import type {
|
|
3
|
-
CreateArg,
|
|
4
|
-
CreateResponse,
|
|
5
|
-
DownArg,
|
|
6
|
-
IMigrater,
|
|
7
|
-
SetupArg,
|
|
8
|
-
UpArg,
|
|
9
|
-
} from './abstractions/migrater.ts';
|
|
10
|
-
import type { ISqlDialect } from './abstractions/sql-dialect.ts';
|
|
11
|
-
import { isMigrationName, nextSequence, pad4 } from './create-sequence.ts';
|
|
12
|
-
import { applyOneDown, applyOneUp, discoverMigrations } from './datasource-migrate.ts';
|
|
13
|
-
import { defaultDialectFactory } from './infrastructure/default-factory.ts';
|
|
14
|
-
import { fillTemplate, loadMessage, loadScaffold } from './infrastructure/message-templates.ts';
|
|
15
|
-
|
|
16
|
-
const invalidNameTpl = await loadMessage('errors/invalid-name');
|
|
17
|
-
|
|
18
|
-
export class Migrater implements IMigrater {
|
|
19
|
-
constructor(private readonly dialect: ISqlDialect) {}
|
|
20
|
-
|
|
21
|
-
get name(): string {
|
|
22
|
-
return this.dialect.name;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async setup({ connection, migrationsPath }: SetupArg): Promise<void> {
|
|
26
|
-
await this.dialect.prepareSetup(connection);
|
|
27
|
-
const client = await this.dialect.createClient(connection);
|
|
28
|
-
try {
|
|
29
|
-
await client.exec(this.dialect.migratesDdl);
|
|
30
|
-
await client.exec(this.dialect.migrateLogsDdl);
|
|
31
|
-
await mkdir(migrationsPath, { recursive: true });
|
|
32
|
-
} finally {
|
|
33
|
-
await client.close();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async up({ connection, migrationsPath, one = false }: UpArg): Promise<string[]> {
|
|
38
|
-
const client = await this.dialect.createClient(connection);
|
|
39
|
-
try {
|
|
40
|
-
const migrations = await discoverMigrations({
|
|
41
|
-
migratePath: migrationsPath,
|
|
42
|
-
dialect: this.dialect.name,
|
|
43
|
-
});
|
|
44
|
-
const applied: string[] = [];
|
|
45
|
-
for (;;) {
|
|
46
|
-
const result = await applyOneUp({ client, migrations });
|
|
47
|
-
if (!result.applied || result.name == null) break;
|
|
48
|
-
applied.push(result.name);
|
|
49
|
-
if (one) break;
|
|
50
|
-
}
|
|
51
|
-
return applied;
|
|
52
|
-
} finally {
|
|
53
|
-
await client.close();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async down({ connection, migrationsPath }: DownArg): Promise<string | null> {
|
|
58
|
-
const client = await this.dialect.createClient(connection);
|
|
59
|
-
try {
|
|
60
|
-
const migrations = await discoverMigrations({
|
|
61
|
-
migratePath: migrationsPath,
|
|
62
|
-
dialect: this.dialect.name,
|
|
63
|
-
});
|
|
64
|
-
const result = await applyOneDown({ client, migrations });
|
|
65
|
-
return result.rolledBack ? result.name : null;
|
|
66
|
-
} finally {
|
|
67
|
-
await client.close();
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async create({ name, migrationsPath }: CreateArg): Promise<CreateResponse> {
|
|
72
|
-
if (!isMigrationName(name)) {
|
|
73
|
-
throw new Error(fillTemplate(invalidNameTpl, { name }));
|
|
74
|
-
}
|
|
75
|
-
await mkdir(migrationsPath, { recursive: true });
|
|
76
|
-
let entries: string[] = [];
|
|
77
|
-
try {
|
|
78
|
-
entries = await readdir(migrationsPath);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
if (!(err instanceof Error && 'code' in err && err.code === 'ENOENT')) throw err;
|
|
81
|
-
}
|
|
82
|
-
const seq = pad4(nextSequence(entries));
|
|
83
|
-
const upName = `${seq}_${name}_up.sql`;
|
|
84
|
-
const downName = `${seq}_${name}_down.sql`;
|
|
85
|
-
await writeFile(`${migrationsPath}/${upName}`, await loadScaffold('up', { name }));
|
|
86
|
-
await writeFile(`${migrationsPath}/${downName}`, await loadScaffold('down', { name }));
|
|
87
|
-
return { upName, downName };
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export const getMigrater = (provider: string): IMigrater =>
|
|
92
|
-
new Migrater(defaultDialectFactory.get(provider));
|
package/src/migration-client.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export type MigrationRowValue = string | number | bigint | boolean | Date | null;
|
|
2
|
-
export type MigrationRow = Record<string, MigrationRowValue>;
|
|
3
|
-
export type MigrationSqlParam = string | number | bigint | boolean | Date | null;
|
|
4
|
-
|
|
5
|
-
/** The subset of a better-sqlite3 Database that `insertLogStarted` reaches through `_raw` when a caller injects a synchronous handle. */
|
|
6
|
-
export interface SqliteRawDatabase {
|
|
7
|
-
prepare(source: string): {
|
|
8
|
-
run(...params: MigrationSqlParam[]): { lastInsertRowid: number | bigint };
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Provider-agnostic migration client used by IMigrater.up, IMigrater.down, and IMigrater.setup. */
|
|
13
|
-
export interface MigrationClient {
|
|
14
|
-
dialect: import('./sql.ts').SqlDialect;
|
|
15
|
-
exec(sql: string): Promise<void>;
|
|
16
|
-
query(sql: string, params?: MigrationSqlParam[]): Promise<MigrationRow[]>;
|
|
17
|
-
run(sql: string, params?: MigrationSqlParam[]): Promise<void>;
|
|
18
|
-
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
19
|
-
close(): Promise<void>;
|
|
20
|
-
_raw?: SqliteRawDatabase;
|
|
21
|
-
}
|
package/src/path-exists.ts
DELETED
package/src/setup-sql.test.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getMigrater } from './migrater.ts';
|
|
3
|
-
import { defaultDialectFactory } from './infrastructure/default-factory.ts';
|
|
4
|
-
|
|
5
|
-
describe('IMigrater setup bookkeeping', () => {
|
|
6
|
-
it('loads the shared DDL templates for every dialect', () => {
|
|
7
|
-
for (const dialect of defaultDialectFactory.all()) {
|
|
8
|
-
expect(dialect.migratesDdl).toMatch(/migrates/i);
|
|
9
|
-
expect(dialect.migrateLogsDdl).toMatch(/migrate_logs/i);
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('rejects an unknown dialect', () => {
|
|
14
|
-
expect(() => getMigrater('dbase')).toThrow(/Unknown SQL dialect/);
|
|
15
|
-
});
|
|
16
|
-
});
|
package/src/sql.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { cliSpec } from './cli-spec.ts';
|
|
2
|
-
|
|
3
|
-
export type SqlDialect = 'sqlite' | 'mysql' | 'postgres' | 'sqlserver' | 'oracle';
|
|
4
|
-
|
|
5
|
-
const stripKey = (raw: string): string => raw.toLowerCase().replace(/[\s_\-]/g, '');
|
|
6
|
-
|
|
7
|
-
export const normalizeDialect = (raw: string | null | undefined): SqlDialect | null => {
|
|
8
|
-
if (!raw) return null;
|
|
9
|
-
const key = stripKey(raw);
|
|
10
|
-
if ((cliSpec.providers as string[]).includes(key)) return key as SqlDialect;
|
|
11
|
-
for (const [alias, canonical] of Object.entries(cliSpec.provider_aliases ?? {})) {
|
|
12
|
-
if (stripKey(alias) === key) return canonical as SqlDialect;
|
|
13
|
-
}
|
|
14
|
-
return null;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export { pathExists } from './path-exists.ts';
|