@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.
Files changed (55) hide show
  1. package/README.md +1 -0
  2. package/cli.yaml +264 -0
  3. package/dist/bin/migrate-create.js +0 -0
  4. package/dist/bin/migrate-down.js +0 -0
  5. package/dist/bin/migrate-init.js +0 -0
  6. package/dist/bin/migrate-setup.js +0 -0
  7. package/dist/bin/migrate-up.js +0 -0
  8. package/package.json +7 -6
  9. package/src/abstractions/migrater.ts +0 -13
  10. package/src/abstractions/sql-dialect-base.ts +0 -36
  11. package/src/abstractions/sql-dialect-factory.ts +0 -29
  12. package/src/abstractions/sql-dialect.ts +0 -17
  13. package/src/bin/migrate-create.ts +0 -80
  14. package/src/bin/migrate-down.ts +0 -163
  15. package/src/bin/migrate-init.ts +0 -88
  16. package/src/bin/migrate-setup.ts +0 -98
  17. package/src/bin/migrate-up.ts +0 -111
  18. package/src/cli-contract.ts +0 -25
  19. package/src/cli-spec.ts +0 -64
  20. package/src/cli.ts +0 -52
  21. package/src/create-sequence.ts +0 -17
  22. package/src/datasource-migrate.ts +0 -314
  23. package/src/dialects/mysql-dialect.test.ts +0 -93
  24. package/src/dialects/mysql-dialect.ts +0 -67
  25. package/src/dialects/oracle-dialect.test.ts +0 -118
  26. package/src/dialects/oracle-dialect.ts +0 -91
  27. package/src/dialects/postgres-dialect.test.ts +0 -80
  28. package/src/dialects/postgres-dialect.ts +0 -65
  29. package/src/dialects/sqlite-dialect.rollback.test.ts +0 -30
  30. package/src/dialects/sqlite-dialect.test.ts +0 -77
  31. package/src/dialects/sqlite-dialect.ts +0 -96
  32. package/src/dialects/sqlserver-dialect.test.ts +0 -99
  33. package/src/dialects/sqlserver-dialect.ts +0 -87
  34. package/src/generate-help.test.ts +0 -33
  35. package/src/generate-help.ts +0 -173
  36. package/src/help.test.ts +0 -48
  37. package/src/help.ts +0 -12
  38. package/src/index.ts +0 -23
  39. package/src/infrastructure/connection-resolver.ts +0 -18
  40. package/src/infrastructure/default-factory.test.ts +0 -18
  41. package/src/infrastructure/default-factory.ts +0 -17
  42. package/src/infrastructure/interop-default.test.ts +0 -9
  43. package/src/infrastructure/interop-default.ts +0 -2
  44. package/src/infrastructure/message-templates.ts +0 -30
  45. package/src/infrastructure/split-on-go.ts +0 -19
  46. package/src/infrastructure/sql-templates.ts +0 -10
  47. package/src/infrastructure/templates-root.ts +0 -13
  48. package/src/init-select.test.ts +0 -26
  49. package/src/init-select.ts +0 -73
  50. package/src/migrater.ts +0 -92
  51. package/src/migration-client.ts +0 -21
  52. package/src/path-exists.ts +0 -10
  53. package/src/setup-sql.test.ts +0 -16
  54. package/src/sql.ts +0 -17
  55. package/tsconfig.build.json +0 -19
package/README.md ADDED
@@ -0,0 +1 @@
1
+ Migrate executables for deterministic-code.
package/cli.yaml ADDED
@@ -0,0 +1,264 @@
1
+ # Source of truth for migrate command names, argv layout, and --help copy.
2
+ # TypeScript, Rust, and C# parsers must match this file.
3
+ # IMigrater (setup, up, down, create) lives in api.yaml. SQL spelling is a provider detail.
4
+ # Help templates in shared/templates/help are generated:
5
+ # npm run generate:help
6
+ # Usage line for each verb: `Usage: migrate-<verb> ` + args in order.
7
+
8
+ command: migrate-{verb}
9
+
10
+ providers:
11
+ - sqlite
12
+ - postgres
13
+ - mysql
14
+ - sqlserver
15
+ - oracle
16
+
17
+ # Lookup: lowercase, strip spaces/underscores/hyphens, then match a provider or alias.
18
+ provider_aliases:
19
+ sqlite3: sqlite
20
+ mariadb: mysql
21
+ postgresql: postgres
22
+ pg: postgres
23
+ mssql: sqlserver
24
+ mssqlserver: sqlserver
25
+ ora: oracle
26
+
27
+ exit_codes:
28
+ success: 0
29
+ usage: 2
30
+ runtime: 1
31
+
32
+ help:
33
+ - -h
34
+ - --help
35
+
36
+ help_stream: stderr
37
+
38
+ # Canonical argv is `--flag value`. `--flag=value` is optional extra syntax.
39
+ aliases:
40
+ --migrate-path: --migrations-path
41
+ --migrate-root: --migrations-root
42
+
43
+ env_var_list_separator: " / "
44
+
45
+ migrations:
46
+ path_flag: --migrations-path
47
+ root_flag: --migrations-root
48
+ root_default: sql
49
+ layout: "<root>/<provider>/migrations"
50
+ discovery: top_level_up_down_pairs
51
+ missing_directory: errors/migrate-path-not-dir
52
+
53
+ create:
54
+ name_pattern: "^[a-z][a-z0-9_]*$"
55
+ sequence_digits: 4
56
+
57
+ checksum:
58
+ algorithm: sha256
59
+ drift_bypass: MIGRATE_ALLOW_CHECKSUM_DRIFT
60
+
61
+ # Documented when a command sets show_connection_env: true.
62
+ connection_environment:
63
+ sqlite: [SQLITE_PATH, DB_PATH]
64
+ postgres: [PG_CONNECTION_STRING, DATABASE_URL]
65
+ mysql: [MYSQL_URL, DATABASE_URL]
66
+ sqlserver: [MSSQL_URL, DATABASE_URL]
67
+ oracle: [ORACLE_CONNECT_STRING, DATABASE_URL]
68
+
69
+ commands:
70
+ - verb: init
71
+ summary: |
72
+ Selects the dialects this tree will compile and install. Repeat --dialect
73
+ once per provider. Re-running replaces the previous set. --all restores
74
+ every dialect. Does not take --connection.
75
+ examples: |
76
+ {{command}} --dialect sqlite --dialect postgres
77
+ {{command}} --dialect oracle
78
+ {{command}} --all
79
+ args:
80
+ - flag: --dialect
81
+ placeholder: providers
82
+ required: false
83
+ help: "Dialect to enable. Repeat for each dialect. Aliases from this file apply."
84
+ - flag: --all
85
+ required: false
86
+ help: "Enable every dialect. Restores the committed full-provider set."
87
+
88
+ - verb: setup
89
+ show_connection_env: true
90
+ summary: |
91
+ Creates the bookkeeping tables (migrates, migrate_logs) for the chosen dialect
92
+ and the migrations directory (mkdir -p). Idempotent — re-running against an
93
+ already-setup database is a no-op. For sqlite, also mkdir -p of the parent
94
+ directory of --connection so a fresh-tree path like ./.test/foo.sqlite works.
95
+ examples: |
96
+ # sqlite — bare path or sqlite:// URL; both create a file on disk.
97
+ {{command}} --provider sqlite --connection ./app.sqlite
98
+ {{command}} --provider sqlite --connection sqlite:///absolute/path/app.sqlite
99
+
100
+ # postgres — URL or keyword form.
101
+ {{command}} --provider postgres --connection postgresql://user:pass@host:5432/dbname
102
+
103
+ # mysql — URL form.
104
+ {{command}} --provider mysql --connection mysql://user:pass@host:3306/dbname
105
+
106
+ # sqlserver / oracle — pass a driver-native connection string.
107
+ {{command}} --provider sqlserver --connection "Server=host;Database=dbname;User Id=user;Password=pass;"
108
+ {{command}} --provider oracle --connection "user/pass@host:1521/service"
109
+
110
+ # env-var fallback when --connection is omitted:
111
+ SQLITE_PATH=./app.sqlite {{command}} --provider sqlite
112
+ DATABASE_URL=postgresql://... {{command}} --provider postgres
113
+ DATABASE_URL=mysql://... {{command}} --provider mysql
114
+
115
+ # explicit in-memory sqlite:
116
+ {{command}} --provider sqlite --connection :memory:
117
+ args:
118
+ - flag: --provider
119
+ placeholder: providers
120
+ required: true
121
+ help: "Database dialect. Required."
122
+ - flag: --connection
123
+ placeholder: url
124
+ required: true
125
+ help: "Connection string. Required (or set the per-dialect env var below)."
126
+ - flag: --migrations-path
127
+ placeholder: dir
128
+ required: false
129
+ help: "Migrations directory (default: ./sql/<dialect>/migrations)."
130
+ - flag: --and-up
131
+ required: false
132
+ help: "After setup, immediately invoke migrate-up so the chain is applied in one step."
133
+
134
+ - verb: up
135
+ show_connection_env: true
136
+ summary: |
137
+ Applies ALL pending migrations in sequence, each in its own transaction.
138
+ examples: |
139
+ # sqlite — bare path or sqlite:// URL; both create a file on disk.
140
+ {{command}} --provider sqlite --connection ./app.sqlite
141
+ {{command}} --provider sqlite --connection sqlite:///absolute/path/app.sqlite
142
+
143
+ # postgres — URL or keyword form.
144
+ {{command}} --provider postgres --connection postgresql://user:pass@host:5432/dbname
145
+
146
+ # mysql — URL form.
147
+ {{command}} --provider mysql --connection mysql://user:pass@host:3306/dbname
148
+
149
+ # sqlserver / oracle — pass a driver-native connection string.
150
+ {{command}} --provider sqlserver --connection "Server=host;Database=dbname;User Id=user;Password=pass;"
151
+ {{command}} --provider oracle --connection "user/pass@host:1521/service"
152
+
153
+ # env-var fallback when --connection is omitted:
154
+ SQLITE_PATH=./app.sqlite {{command}} --provider sqlite
155
+ DATABASE_URL=postgresql://... {{command}} --provider postgres
156
+
157
+ # custom migrations layout (default looks under sql/<dialect>/migrations):
158
+ {{command}} --provider sqlite --connection ./app.sqlite --migrations-path ./db/changes/sqlite
159
+ {{command}} --provider sqlite --connection ./app.sqlite --migrations-root ./db/changes
160
+
161
+ # one-shot: apply the next pending migration only, then exit.
162
+ {{command}} --provider sqlite --connection ./app.sqlite --one
163
+ args:
164
+ - flag: --provider
165
+ placeholder: providers
166
+ required: true
167
+ help: "Database dialect. Required."
168
+ - flag: --connection
169
+ placeholder: url
170
+ required: true
171
+ help: "Connection string. Required (or set the per-dialect env var below)."
172
+ - flag: --migrations-path
173
+ placeholder: dir
174
+ required: false
175
+ help: "Full path to the migrations directory (e.g. sql/sqlite/migrations)."
176
+ - flag: --migrations-root
177
+ placeholder: dir
178
+ required: false
179
+ help: |
180
+ Root prefix; the script then looks at <migrations-root>/<dialect>/migrations.
181
+ Defaults to 'sql'. Ignored when --migrations-path is also set.
182
+ - flag: --one
183
+ required: false
184
+ help: "Apply only the next pending migration, then exit. Default is to apply all pending migrations."
185
+
186
+ - verb: down
187
+ show_connection_env: true
188
+ summary: |
189
+ Rolls back the MOST RECENTLY applied migration (one step). DESTRUCTIVE: prints a
190
+ random 4-letter uppercase token on stderr and refuses to proceed unless the
191
+ operator types it back on stdin (or passes --confirm <TOKEN> matching it).
192
+ examples: |
193
+ # sqlite — bare path or sqlite:// URL.
194
+ {{command}} --provider sqlite --connection ./app.sqlite
195
+ {{command}} --provider sqlite --connection sqlite:///absolute/path/app.sqlite
196
+
197
+ # postgres — URL or keyword form.
198
+ {{command}} --provider postgres --connection postgresql://user:pass@host:5432/dbname
199
+
200
+ # mysql — URL form.
201
+ {{command}} --provider mysql --connection mysql://user:pass@host:3306/dbname
202
+
203
+ # sqlserver / oracle — pass a driver-native connection string.
204
+ {{command}} --provider sqlserver --connection "Server=host;Database=dbname;User Id=user;Password=pass;"
205
+ {{command}} --provider oracle --connection "user/pass@host:1521/service"
206
+
207
+ # env-var fallback when --connection is omitted:
208
+ SQLITE_PATH=./app.sqlite {{command}} --provider sqlite
209
+ DATABASE_URL=postgresql://... {{command}} --provider postgres
210
+
211
+ # custom migrations layout (default looks under sql/<dialect>/migrations):
212
+ {{command}} --provider sqlite --connection ./app.sqlite --migrations-path ./db/changes/sqlite
213
+ args:
214
+ - flag: --provider
215
+ placeholder: providers
216
+ required: true
217
+ help: "Database dialect. Required."
218
+ - flag: --connection
219
+ placeholder: url
220
+ required: true
221
+ help: "Connection string. Required (or set the per-dialect env var below)."
222
+ - flag: --migrations-path
223
+ placeholder: dir
224
+ required: false
225
+ help: "Full path to the migrations directory."
226
+ - flag: --migrations-root
227
+ placeholder: dir
228
+ required: false
229
+ help: |
230
+ Root prefix; the script then looks at <migrations-root>/<dialect>/migrations.
231
+ Defaults to 'sql'. Ignored when --migrations-path is also set.
232
+ - flag: --confirm
233
+ placeholder: TOKEN
234
+ required: false
235
+ help: |
236
+ Skip the interactive prompt. The TOKEN must match the value
237
+ printed on stderr at runtime — there is no fixed bypass value.
238
+
239
+ - verb: create
240
+ summary: |
241
+ Creates a new <NNNN>_<name>_up.sql + <NNNN>_<name>_down.sql pair scaffolded
242
+ with TODO placeholders. NNNN is one greater than the highest 1-4-digit
243
+ sequence prefix already present in the migrations directory (starts at 0001).
244
+ Legacy timestamp-prefixed filenames are ignored for numbering.
245
+ examples: |
246
+ {{command}} --provider sqlite --name add_users
247
+ {{command}} --provider postgres --name backfill_email_lower --migrations-path ./db/changes/postgres
248
+ args:
249
+ - flag: --provider
250
+ placeholder: providers
251
+ required: true
252
+ help: "Database dialect. Required."
253
+ - flag: --name
254
+ placeholder: snake_case_slug
255
+ required: true
256
+ help: |
257
+ snake_case slug for the new migration. Required. Must
258
+ match /^[a-z][a-z0-9_]*$/.
259
+ - flag: --migrations-path
260
+ placeholder: dir
261
+ required: false
262
+ help: |
263
+ Migrations directory (default: ./sql/<dialect>/migrations).
264
+ Created with mkdir -p if it does not exist.
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deterministic-code/migraters",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "TypeScript migrate executables for deterministic-code.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,14 +19,11 @@
19
19
  },
20
20
  "files": [
21
21
  "dist",
22
- "src",
23
22
  "cli.yaml",
24
- "bundle.yaml",
25
- "tsconfig.build.json"
23
+ "bundle.yaml"
26
24
  ],
27
25
  "scripts": {
28
26
  "build": "rm -rf dist && tsc -p tsconfig.build.json && cp -R ../shared/templates dist/templates",
29
- "prepare": "npm run build",
30
27
  "typecheck": "tsc -p tsconfig.build.json --noEmit",
31
28
  "test": "vitest run --coverage",
32
29
  "test:proof": "vitest run --config vitest.proof.config.ts",
@@ -76,5 +73,9 @@
76
73
  "prettier": "^3.9.6",
77
74
  "typescript": "^5.8.0",
78
75
  "vitest": "^3.2.4"
79
- }
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ },
80
+ "license": "MIT"
80
81
  }
@@ -1,13 +0,0 @@
1
- export type SetupArg = { connection: string; migrationsPath: string };
2
- export type UpArg = { connection: string; migrationsPath: string; one?: boolean };
3
- export type DownArg = { connection: string; migrationsPath: string };
4
- export type CreateArg = { name: string; migrationsPath: string };
5
- export type CreateResponse = { upName: string; downName: string };
6
-
7
- export interface IMigrater {
8
- readonly name: string;
9
- setup(args: SetupArg): Promise<void>;
10
- up(args: UpArg): Promise<string[]>;
11
- down(args: DownArg): Promise<string | null>;
12
- create(args: CreateArg): Promise<CreateResponse>;
13
- }
@@ -1,36 +0,0 @@
1
- import type { SqlDialect } from '../sql.ts';
2
- import type { ISqlDialect } from './sql-dialect.ts';
3
-
4
- export abstract class SqlDialectBase implements ISqlDialect {
5
- abstract readonly name: SqlDialect;
6
- abstract readonly connectionEnvironmentVariables: readonly string[];
7
- abstract readonly migratesDdl: string;
8
- abstract readonly migrateLogsDdl: string;
9
- readonly usesLastInsertRowid: boolean = false;
10
-
11
- quoteIdent(ident: string): string {
12
- return `"${ident}"`;
13
- }
14
-
15
- nowExpr(): string {
16
- return 'CURRENT_TIMESTAMP';
17
- }
18
-
19
- limitClause(n: number): string {
20
- return `LIMIT ${n}`;
21
- }
22
-
23
- translatePlaceholders(sql: string): string {
24
- return sql;
25
- }
26
-
27
- async prerequisiteError(_connection: string): Promise<string | null> {
28
- return null;
29
- }
30
-
31
- async prepareSetup(_connection: string): Promise<void> {}
32
-
33
- abstract createClient(
34
- connection: string,
35
- ): Promise<import('../migration-client.ts').MigrationClient>;
36
- }
@@ -1,29 +0,0 @@
1
- import { normalizeDialect, type SqlDialect } from '../sql.ts';
2
- import type { ISqlDialect } from './sql-dialect.ts';
3
-
4
- const VALID = 'sqlite, mysql, postgres, sqlserver, oracle';
5
-
6
- export class SqlDialectFactory {
7
- private readonly byName: Map<SqlDialect, ISqlDialect>;
8
-
9
- constructor(dialects: readonly ISqlDialect[]) {
10
- this.byName = new Map(dialects.map((d) => [d.name, d]));
11
- }
12
-
13
- tryGet(raw: string): ISqlDialect | undefined {
14
- const key = normalizeDialect(raw);
15
- return key ? this.byName.get(key) : undefined;
16
- }
17
-
18
- get(raw: string): ISqlDialect {
19
- const dialect = this.tryGet(raw);
20
- if (!dialect) {
21
- throw new Error(`Unknown SQL dialect "${raw}". Valid: ${VALID}.`);
22
- }
23
- return dialect;
24
- }
25
-
26
- all(): readonly ISqlDialect[] {
27
- return [...this.byName.values()];
28
- }
29
- }
@@ -1,17 +0,0 @@
1
- import type { MigrationClient } from '../migration-client.ts';
2
- import type { SqlDialect } from '../sql.ts';
3
-
4
- export interface ISqlDialect {
5
- readonly name: SqlDialect;
6
- readonly connectionEnvironmentVariables: readonly string[];
7
- readonly migratesDdl: string;
8
- readonly migrateLogsDdl: string;
9
- readonly usesLastInsertRowid: boolean;
10
- quoteIdent(ident: string): string;
11
- nowExpr(): string;
12
- limitClause(n: number): string;
13
- translatePlaceholders(sql: string): string;
14
- prerequisiteError(connection: string): Promise<string | null>;
15
- prepareSetup(connection: string): Promise<void>;
16
- createClient(connection: string): Promise<MigrationClient>;
17
- }
@@ -1,80 +0,0 @@
1
- #!/usr/bin/env node
2
- // Scaffolds a new <NNNN>_<name>_{up,down}.sql pair in the migrations directory. Numbering is max(1-4-digit prefix in dir)+1; legacy timestamp prefixes are ignored.
3
-
4
- import { missingProviderMsg, requireDialect, takeFlag } from '../cli.ts';
5
- import { isMigrationName } from '../create-sequence.ts';
6
- import { fillTemplate, loadMessage } from '../infrastructure/message-templates.ts';
7
- import { loadHelp, Migrater } from '../index.ts';
8
-
9
- const [HELP_TEXT, unknownArgTpl, missingNameMsg, invalidNameTpl, createdTpl] = await Promise.all([
10
- loadHelp('migrate-create'),
11
- loadMessage('errors/unknown-arg'),
12
- loadMessage('errors/missing-name'),
13
- loadMessage('errors/invalid-name'),
14
- loadMessage('status/created'),
15
- ]);
16
-
17
- const parseArgs = (argv: string[]) => {
18
- const args: {
19
- provider: string | null;
20
- name: string | null;
21
- migrationsPath: string | null;
22
- } = { provider: null, name: null, migrationsPath: null };
23
- const rest = argv.slice(2);
24
- for (let i = 0; i < rest.length; i++) {
25
- const a = rest[i] as string;
26
- if (a === '-h' || a === '--help') {
27
- process.stderr.write(HELP_TEXT);
28
- process.exit(0);
29
- }
30
- let taken;
31
- if ((taken = takeFlag(rest, i, a, '--provider'))) {
32
- args.provider = taken.value;
33
- i = taken.next;
34
- } else if ((taken = takeFlag(rest, i, a, '--name'))) {
35
- args.name = taken.value;
36
- i = taken.next;
37
- } else if (
38
- (taken = takeFlag(rest, i, a, '--migrations-path')) ||
39
- (taken = takeFlag(rest, i, a, '--migrate-path'))
40
- ) {
41
- args.migrationsPath = taken.value;
42
- i = taken.next;
43
- } else {
44
- process.stderr.write(fillTemplate(unknownArgTpl, { arg: a }));
45
- process.exit(2);
46
- }
47
- }
48
- return args;
49
- };
50
-
51
- const main = async () => {
52
- const args = parseArgs(process.argv);
53
- if (!args.provider) {
54
- process.stderr.write(missingProviderMsg);
55
- process.exit(2);
56
- }
57
- const dialect = requireDialect(args.provider);
58
- if (!args.name) {
59
- process.stderr.write(missingNameMsg);
60
- process.exit(2);
61
- }
62
- if (!isMigrationName(args.name)) {
63
- process.stderr.write(fillTemplate(invalidNameTpl, { name: args.name }));
64
- process.exit(2);
65
- }
66
-
67
- const migrationsPath = args.migrationsPath ?? `./sql/${dialect.name}/migrations`;
68
- const { upName, downName } = await new Migrater(dialect).create({
69
- name: args.name,
70
- migrationsPath,
71
- });
72
- process.stdout.write(fillTemplate(createdTpl, { file: upName }));
73
- process.stdout.write(fillTemplate(createdTpl, { file: downName }));
74
- };
75
-
76
- main().catch((e: unknown) => {
77
- const err = e as Error;
78
- process.stderr.write(`${err?.stack ?? err?.message ?? String(e)}\n`);
79
- process.exit(1);
80
- });
@@ -1,163 +0,0 @@
1
- #!/usr/bin/env node
2
- // Rolls back the most recently applied migration (one step). DESTRUCTIVE: prints a random 4-letter uppercase confirmation token and refuses to proceed unless the user types it back on stdin. Pass --confirm <TOKEN> matching the printed token to bypass the interactive prompt (CI / scripted use).
3
-
4
- import { resolve } from 'node:path';
5
- import { createInterface } from 'node:readline/promises';
6
- import { missingProviderMsg, requireConnection, requireDialect, takeFlag } from '../cli.ts';
7
- import { fillTemplate, loadMessage } from '../infrastructure/message-templates.ts';
8
- import { loadHelp, Migrater } from '../index.ts';
9
-
10
- const [
11
- HELP_TEXT,
12
- unknownArgTpl,
13
- destructiveHeaderTpl,
14
- confirmMismatchTpl,
15
- confirmTtyRequiredTpl,
16
- confirmPromptTpl,
17
- confirmTokenMismatchMsg,
18
- noRollbackMsg,
19
- rolledBackTpl,
20
- ] = await Promise.all([
21
- loadHelp('migrate-down'),
22
- loadMessage('errors/unknown-arg'),
23
- loadMessage('down/destructive-header'),
24
- loadMessage('down/confirm-mismatch'),
25
- loadMessage('down/confirm-tty-required'),
26
- loadMessage('down/confirm-prompt'),
27
- loadMessage('down/confirm-token-mismatch'),
28
- loadMessage('status/no-rollback'),
29
- loadMessage('status/rolled-back'),
30
- ]);
31
-
32
- const parseArgs = (argv: string[]) => {
33
- const args: {
34
- provider: string | null;
35
- migratePath: string | null;
36
- migrateRoot: string | null;
37
- connection: string | null;
38
- confirm: string | null;
39
- } = {
40
- provider: null,
41
- migratePath: null,
42
- migrateRoot: null,
43
- connection: null,
44
- confirm: null,
45
- };
46
- const rest = argv.slice(2);
47
- for (let i = 0; i < rest.length; i++) {
48
- const a = rest[i] as string;
49
- if (a === '-h' || a === '--help') {
50
- process.stderr.write(HELP_TEXT);
51
- process.exit(0);
52
- }
53
- let taken;
54
- if ((taken = takeFlag(rest, i, a, '--provider'))) {
55
- args.provider = taken.value;
56
- i = taken.next;
57
- } else if (
58
- (taken = takeFlag(rest, i, a, '--migrations-path')) ||
59
- (taken = takeFlag(rest, i, a, '--migrate-path'))
60
- ) {
61
- args.migratePath = taken.value;
62
- i = taken.next;
63
- } else if (
64
- (taken = takeFlag(rest, i, a, '--migrations-root')) ||
65
- (taken = takeFlag(rest, i, a, '--migrate-root'))
66
- ) {
67
- args.migrateRoot = taken.value;
68
- i = taken.next;
69
- } else if ((taken = takeFlag(rest, i, a, '--connection'))) {
70
- args.connection = taken.value;
71
- i = taken.next;
72
- } else if ((taken = takeFlag(rest, i, a, '--confirm'))) {
73
- args.confirm = taken.value;
74
- i = taken.next;
75
- } else {
76
- process.stderr.write(fillTemplate(unknownArgTpl, { arg: a }));
77
- process.exit(2);
78
- }
79
- }
80
- return args;
81
- };
82
-
83
- const resolveMigratePath = (
84
- args: { migratePath: string | null; migrateRoot: string | null },
85
- dialect: string,
86
- ): string => {
87
- if (args.migratePath) return args.migratePath;
88
- const root = args.migrateRoot ?? 'sql';
89
- return `${root}/${dialect}/migrations`;
90
- };
91
-
92
- const randomToken = (): string => {
93
- const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
94
- const buf = new Uint8Array(4);
95
- globalThis.crypto.getRandomValues(buf);
96
- let out = '';
97
- for (let i = 0; i < 4; i++) out += alphabet[(buf[i] as number) % alphabet.length];
98
- return out;
99
- };
100
-
101
- const promptConfirmation = async (token: string): Promise<boolean> => {
102
- if (!process.stdin.isTTY) {
103
- process.stderr.write(fillTemplate(confirmTtyRequiredTpl, { token }));
104
- process.exit(2);
105
- }
106
- const rl = createInterface({ input: process.stdin, output: process.stderr });
107
- try {
108
- const answer = await rl.question(fillTemplate(confirmPromptTpl, { token }));
109
- return answer.trim() === token;
110
- } finally {
111
- rl.close();
112
- }
113
- };
114
-
115
- const main = async () => {
116
- const args = parseArgs(process.argv);
117
- if (!args.provider) {
118
- process.stderr.write(missingProviderMsg);
119
- process.exit(2);
120
- }
121
- const dialect = requireDialect(args.provider);
122
- const connection = requireConnection(dialect, args.connection);
123
-
124
- const missing = await dialect.prerequisiteError(connection);
125
- if (missing !== null) {
126
- process.stderr.write(`${missing}\n`);
127
- process.exit(2);
128
- }
129
-
130
- const token = randomToken();
131
- process.stderr.write(
132
- fillTemplate(destructiveHeaderTpl, { dialect: dialect.name, token }),
133
- );
134
-
135
- if (args.confirm !== null) {
136
- if (args.confirm !== token) {
137
- process.stderr.write(
138
- fillTemplate(confirmMismatchTpl, { supplied: args.confirm, token }),
139
- );
140
- process.exit(2);
141
- }
142
- } else {
143
- const ok = await promptConfirmation(token);
144
- if (!ok) {
145
- process.stderr.write(confirmTokenMismatchMsg);
146
- process.exit(2);
147
- }
148
- }
149
-
150
- const migratePath = resolve(resolveMigratePath(args, dialect.name));
151
- const name = await new Migrater(dialect).down({
152
- connection,
153
- migrationsPath: migratePath,
154
- });
155
- if (name == null) process.stdout.write(noRollbackMsg);
156
- else process.stdout.write(fillTemplate(rolledBackTpl, { name }));
157
- };
158
-
159
- main().catch((e: unknown) => {
160
- const err = e as Error;
161
- process.stderr.write(`${err?.stack ?? err?.message ?? String(e)}\n`);
162
- process.exit(1);
163
- });