@maestro-js/db-migrate 1.0.0-alpha.0

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 ADDED
@@ -0,0 +1,307 @@
1
+ # DbMigrations
2
+
3
+ ## Overview
4
+
5
+ The `@maestro-js/db-migrate` package is a standalone migration runner for managing database schema changes. It discovers timestamped migration files from a directory, tracks applied migrations in a database table, and provides up/down/reset/status/scaffold operations with transaction safety and dry-run support.
6
+
7
+ ## Quick Setup
8
+
9
+ ```ts
10
+ import { DbMigrations } from '@maestro-js/db-migrate'
11
+
12
+ // Run all pending migrations against a database connection
13
+ const results = await DbMigrations.migrate(db, './migrations')
14
+
15
+ // Check current migration status
16
+ const statuses = await DbMigrations.status(db, './migrations')
17
+ ```
18
+
19
+ The `db` parameter accepts any object matching the `DbMigrations.Db` interface -- a structural subset of `Db.DbService` from `@maestro-js/db`.
20
+
21
+ ## Migration File Format
22
+
23
+ Migration files must follow the naming pattern `{14-digit-timestamp}_{name}.[jt]s` (e.g., `20240615120000_create_users.ts`). Each file must export `up(db)` and `down(db)` functions.
24
+
25
+ ### Scaffolded migration structure
26
+
27
+ Running `DbMigrations.scaffold({ name: 'create_users', dir: './migrations' })` creates:
28
+
29
+ ```
30
+ migrations/
31
+ 20240615120000_create_users.ts
32
+ sqls/
33
+ 20240615120000_create_users-up.sql
34
+ 20240615120000_create_users-down.sql
35
+ ```
36
+
37
+ The generated `.ts` file reads companion SQL files at runtime:
38
+
39
+ ```ts
40
+ import type { DbMigrations } from '@maestro-js/db-migrate'
41
+ import { readFile } from 'node:fs/promises'
42
+ import { join, dirname } from 'node:path'
43
+ import { fileURLToPath } from 'node:url'
44
+
45
+ const __dirname = dirname(fileURLToPath(import.meta.url))
46
+
47
+ export async function up(db: DbMigrations.Db) {
48
+ const sql = await readFile(join(__dirname, 'sqls', '20240615120000_create_users-up.sql'), 'utf-8')
49
+ await db.statement(sql)
50
+ }
51
+
52
+ export async function down(db: DbMigrations.Db) {
53
+ const sql = await readFile(join(__dirname, 'sqls', '20240615120000_create_users-down.sql'), 'utf-8')
54
+ await db.statement(sql)
55
+ }
56
+ ```
57
+
58
+ ### Manual migration (no SQL files)
59
+
60
+ Write `up` and `down` directly in the `.ts` file:
61
+
62
+ ```ts
63
+ import type { DbMigrations } from '@maestro-js/db-migrate'
64
+
65
+ export async function up(db: DbMigrations.Db) {
66
+ await db.statement(`
67
+ CREATE TABLE users (
68
+ id INT NOT NULL AUTO_INCREMENT,
69
+ email VARCHAR(255) NOT NULL,
70
+ PRIMARY KEY (id)
71
+ )
72
+ `)
73
+ }
74
+
75
+ export async function down(db: DbMigrations.Db) {
76
+ await db.statement('DROP TABLE users')
77
+ }
78
+ ```
79
+
80
+ ## API Reference
81
+
82
+ ### DbMigrations.migrate(db, migrationsDir, options?)
83
+
84
+ Run all pending migrations in timestamp order. Discovers migration files, compares against the tracking table, and executes each pending `up()` within a transaction.
85
+
86
+ ```ts
87
+ migrate(
88
+ db: DbMigrations.Db,
89
+ migrationsDir: string,
90
+ options?: DbMigrations.RunOptions
91
+ ): Promise<DbMigrations.ExecutionResult[]>
92
+ ```
93
+
94
+ **RunOptions:**
95
+ - `count?: number` -- limit how many pending migrations to run
96
+ - `dryRun?: boolean` -- preview without executing (default `false`)
97
+ - `tableName?: string` -- tracking table name (default `'migrations'`)
98
+
99
+ ```ts
100
+ // Run all pending
101
+ const results = await DbMigrations.migrate(db, './migrations')
102
+
103
+ // Run only the next 2 pending migrations
104
+ const results = await DbMigrations.migrate(db, './migrations', { count: 2 })
105
+
106
+ // Dry-run to preview what would run
107
+ const results = await DbMigrations.migrate(db, './migrations', { dryRun: true })
108
+ ```
109
+
110
+ ### DbMigrations.rollback(db, migrationsDir, options?)
111
+
112
+ Revert applied migrations in reverse chronological order. Defaults to rolling back a single migration when `count` is not specified.
113
+
114
+ ```ts
115
+ rollback(
116
+ db: DbMigrations.Db,
117
+ migrationsDir: string,
118
+ options?: DbMigrations.RunOptions
119
+ ): Promise<DbMigrations.ExecutionResult[]>
120
+ ```
121
+
122
+ ```ts
123
+ // Rollback the last applied migration
124
+ const results = await DbMigrations.rollback(db, './migrations')
125
+
126
+ // Rollback the last 3 migrations
127
+ const results = await DbMigrations.rollback(db, './migrations', { count: 3 })
128
+ ```
129
+
130
+ ### DbMigrations.reset(db, migrationsDir, options?)
131
+
132
+ Roll back ALL applied migrations. Delegates to `rollback` with the full count of applied migrations.
133
+
134
+ ```ts
135
+ reset(
136
+ db: DbMigrations.Db,
137
+ migrationsDir: string,
138
+ options?: { dryRun?: boolean; tableName?: string }
139
+ ): Promise<DbMigrations.ExecutionResult[]>
140
+ ```
141
+
142
+ ```ts
143
+ const results = await DbMigrations.reset(db, './migrations')
144
+ ```
145
+
146
+ ### DbMigrations.status(db, migrationsDir, options?)
147
+
148
+ Return the status of every discovered migration -- whether it has been applied and when.
149
+
150
+ ```ts
151
+ status(
152
+ db: DbMigrations.Db,
153
+ migrationsDir: string,
154
+ options?: { tableName?: string }
155
+ ): Promise<DbMigrations.MigrationStatus[]>
156
+ ```
157
+
158
+ Each `MigrationStatus` contains:
159
+ - `version: string` -- the 14-digit timestamp
160
+ - `name: string` -- the migration name from the filename
161
+ - `filename: string` -- full filename
162
+ - `applied: boolean` -- whether the migration has been run
163
+ - `run_on: string | null` -- datetime string when applied, or `null`
164
+
165
+ ```ts
166
+ const statuses = await DbMigrations.status(db, './migrations')
167
+ // [
168
+ // { version: '20240101000000', name: 'create_users', applied: true, run_on: '2024-01-15T10:30:00' },
169
+ // { version: '20240102000000', name: 'add_email_index', applied: false, run_on: null }
170
+ // ]
171
+ ```
172
+
173
+ ### DbMigrations.scaffold(options)
174
+
175
+ Create a new migration file and its companion SQL templates. Returns paths of all created files.
176
+
177
+ ```ts
178
+ scaffold(options: DbMigrations.ScaffoldOptions): Promise<{ ts: string; upSql: string; downSql: string }>
179
+ ```
180
+
181
+ **ScaffoldOptions:**
182
+ - `name: string` -- descriptive migration name (e.g., `'create_orders'`)
183
+ - `dir: string` -- directory to create files in
184
+
185
+ ```ts
186
+ const files = await DbMigrations.scaffold({ name: 'create_orders', dir: './migrations' })
187
+ // { ts: './migrations/20240615120000_create_orders.ts',
188
+ // upSql: './migrations/sqls/20240615120000_create_orders-up.sql',
189
+ // downSql: './migrations/sqls/20240615120000_create_orders-down.sql' }
190
+ ```
191
+
192
+ ### DbMigrations.discover(migrationsDir)
193
+
194
+ Scan a directory for migration files matching the `{14-digit-timestamp}_{name}.[jt]s` pattern. Returns them sorted by version. Returns an empty array if the directory does not exist.
195
+
196
+ ```ts
197
+ discover(migrationsDir: string): Promise<DbMigrations.Migration[]>
198
+ ```
199
+
200
+ Each `Migration` contains:
201
+ - `version: string` -- 14-digit timestamp
202
+ - `name: string` -- migration name
203
+ - `filename: string` -- full filename
204
+ - `filepath: string` -- absolute path to the file
205
+
206
+ ## Common Patterns
207
+
208
+ ### Create and run a new migration
209
+
210
+ ```ts
211
+ // 1. Scaffold the migration files
212
+ const files = await DbMigrations.scaffold({ name: 'add_orders_table', dir: './migrations' })
213
+
214
+ // 2. Write SQL in the generated -up.sql and -down.sql files
215
+
216
+ // 3. Run pending migrations
217
+ const results = await DbMigrations.migrate(db, './migrations')
218
+ ```
219
+
220
+ ### Preview before running
221
+
222
+ ```ts
223
+ // Dry-run returns ExecutionResults with dryRun: true and durationMs: 0
224
+ const preview = await DbMigrations.migrate(db, './migrations', { dryRun: true })
225
+ for (const r of preview) {
226
+ console.log(`Would run: ${r.filename} (${r.direction})`)
227
+ }
228
+ ```
229
+
230
+ ### Rollback and re-run during development
231
+
232
+ ```ts
233
+ // Undo the last migration
234
+ await DbMigrations.rollback(db, './migrations')
235
+
236
+ // Edit the migration file, then re-apply
237
+ await DbMigrations.migrate(db, './migrations')
238
+ ```
239
+
240
+ ### Use a custom tracking table
241
+
242
+ ```ts
243
+ // All functions accept a tableName option (default: 'migrations')
244
+ await DbMigrations.migrate(db, './migrations', { tableName: 'schema_migrations' })
245
+ await DbMigrations.status(db, './migrations', { tableName: 'schema_migrations' })
246
+ ```
247
+
248
+ The table name is validated against `/^[a-zA-Z_]\w*$/` to prevent SQL injection.
249
+
250
+ ## Key Types
251
+
252
+ ```ts
253
+ declare namespace DbMigrations {
254
+ /** Database interface required by all migration functions. Structural subset of Db.DbService. */
255
+ interface Db {
256
+ select<T>(query: string, parameters?: any[]): Promise<T[]>
257
+ insert(query: string, parameters?: any[]): Promise<{ affectedRows: number; insertId: number }>
258
+ delete(query: string, parameters?: any[]): Promise<{ affectedRows: number }>
259
+ statement<T>(query: string, parameters?: any[]): Promise<T>
260
+ transaction: <T>(callback: () => Promise<T>) => Promise<T>
261
+ }
262
+
263
+ interface Migration { version: string; name: string; filename: string; filepath: string }
264
+ interface MigrationStatus { version: string; name: string; filename: string; applied: boolean; run_on: string | null }
265
+ interface ExecutionResult { version: string; name: string; filename: string; direction: 'up' | 'down'; durationMs: number; dryRun: boolean }
266
+ interface RunOptions { count?: number; dryRun?: boolean; tableName?: string }
267
+ interface ScaffoldOptions { name: string; dir: string }
268
+ }
269
+ ```
270
+
271
+ ## Cross-Package Integration
272
+
273
+ ### Works with @maestro-js/db
274
+
275
+ The `DbMigrations.Db` interface is a structural subset of `Db.DbService`. Pass a db service instance directly:
276
+
277
+ ```ts
278
+ import { Db } from '@maestro-js/db'
279
+ import { DbMigrations } from '@maestro-js/db-migrate'
280
+
281
+ // The Db service satisfies DbMigrations.Db structurally
282
+ await DbMigrations.migrate(Db, './migrations')
283
+ ```
284
+
285
+ > **Note:** If migration SQL files contain multiple statements (separated by `;`), the database connection must have `multipleStatements: true` enabled in its MySQL configuration.
286
+
287
+ ## Internal Behavior
288
+
289
+ - The tracking table (`migrations` by default) is auto-created via `CREATE TABLE IF NOT EXISTS` on every operation.
290
+ - Each migration `up()` and `down()` runs inside `db.transaction()`. The tracking record insert/delete is part of the same transaction.
291
+ - Migration names are stored in the tracking table as `/{version}_{name}` (prefixed with `/`).
292
+ - The filename pattern regex is `/^(\d{14})_(.+)\.[jt]s$/` -- only `.js` and `.ts` files with exactly 14 leading digits are discovered.
293
+ - `discover` returns an empty array (not an error) when the directory does not exist.
294
+ - Scaffold generates a 14-digit timestamp from the current date/time: `YYYYMMDDHHmmss`.
295
+
296
+ ## Testing
297
+
298
+ Build and test with:
299
+
300
+ ```bash
301
+ pnpm --filter @maestro-js/db-migrate build
302
+ pnpm --filter @maestro-js/db-migrate test
303
+ pnpm --filter @maestro-js/db-migrate typecheck
304
+ ```
305
+
306
+ Source: `packages/db-migrate/src/index.ts`
307
+ Tests: `packages/db-migrate/tests/db-migrate.test.ts`
@@ -0,0 +1,96 @@
1
+ declare const DbMigrations: {
2
+ migrate: typeof migrate;
3
+ rollback: typeof rollback;
4
+ reset: typeof reset;
5
+ status: typeof status;
6
+ scaffold: typeof scaffold;
7
+ discover: typeof discover;
8
+ };
9
+ declare namespace DbMigrations {
10
+ /** Database interface required by all migration functions. Structural subset of `Db.DbService`. */
11
+ interface Db {
12
+ select<T>(query: string, parameters?: any[]): Promise<T[]>;
13
+ insert(query: string, parameters?: any[]): Promise<{
14
+ affectedRows: number;
15
+ insertId: number;
16
+ }>;
17
+ delete(query: string, parameters?: any[]): Promise<{
18
+ affectedRows: number;
19
+ }>;
20
+ /** If migration SQL files contain multiple statements, the connection must have `multipleStatements: true`. */
21
+ statement<T>(query: string, parameters?: any[]): Promise<T>;
22
+ transaction: <T>(callback: () => Promise<T>) => Promise<T>;
23
+ }
24
+ /** Discovered migration file metadata parsed from the filename. */
25
+ interface Migration {
26
+ version: string;
27
+ name: string;
28
+ filename: string;
29
+ filepath: string;
30
+ }
31
+ /** Combined discovery and applied state for a single migration. */
32
+ interface MigrationStatus {
33
+ version: string;
34
+ name: string;
35
+ filename: string;
36
+ applied: boolean;
37
+ run_on: string | null;
38
+ }
39
+ /** Result returned after executing a single migration (up or down). */
40
+ interface ExecutionResult {
41
+ version: string;
42
+ name: string;
43
+ filename: string;
44
+ direction: 'up' | 'down';
45
+ durationMs: number;
46
+ dryRun: boolean;
47
+ }
48
+ /** Options shared by orchestrator functions (`migrate`, `rollback`, `reset`). */
49
+ interface RunOptions {
50
+ count?: number;
51
+ dryRun?: boolean;
52
+ tableName?: string;
53
+ }
54
+ /** Options for scaffolding a new migration. */
55
+ interface ScaffoldOptions {
56
+ name: string;
57
+ dir: string;
58
+ }
59
+ }
60
+ /**
61
+ * Runs all pending migrations in timestamp order.
62
+ * Discovers migration files, compares against the tracking table, and executes
63
+ * each pending `up()` function within a transaction.
64
+ */
65
+ declare function migrate(db: DbMigrations.Db, migrationsDir: string, options?: DbMigrations.RunOptions): Promise<DbMigrations.ExecutionResult[]>;
66
+ /**
67
+ * Reverts applied migrations in reverse chronological order.
68
+ * Defaults to rolling back a single migration when `count` is not specified.
69
+ */
70
+ declare function rollback(db: DbMigrations.Db, migrationsDir: string, options?: DbMigrations.RunOptions): Promise<DbMigrations.ExecutionResult[]>;
71
+ /**
72
+ * Rolls back ALL applied migrations by delegating to {@link rollback} with the full count.
73
+ */
74
+ declare function reset(db: DbMigrations.Db, migrationsDir: string, options?: {
75
+ dryRun?: boolean;
76
+ tableName?: string;
77
+ }): Promise<DbMigrations.ExecutionResult[]>;
78
+ /**
79
+ * Returns the status of every discovered migration — whether it has been applied and when.
80
+ */
81
+ declare function status(db: DbMigrations.Db, migrationsDir: string, options?: {
82
+ tableName?: string;
83
+ }): Promise<DbMigrations.MigrationStatus[]>;
84
+ /**
85
+ * Creates a new migration file and its companion SQL templates.
86
+ * Returns the paths of all created files.
87
+ */
88
+ declare function scaffold(options: DbMigrations.ScaffoldOptions): Promise<{
89
+ ts: string;
90
+ upSql: string;
91
+ downSql: string;
92
+ }>;
93
+ /** Scans a directory for `.js`/`.ts` files matching the `{14-digit-timestamp}_{name}.[jt]s` pattern. */
94
+ declare function discover(migrationsDir: string): Promise<DbMigrations.Migration[]>;
95
+
96
+ export { DbMigrations };
package/dist/index.js ADDED
@@ -0,0 +1,220 @@
1
+ // src/index.ts
2
+ import { mkdir, readdir, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { pathToFileURL } from "url";
5
+ var DbMigrations = {
6
+ migrate,
7
+ rollback,
8
+ reset,
9
+ status,
10
+ scaffold,
11
+ discover
12
+ };
13
+ async function migrate(db, migrationsDir, options) {
14
+ const tableName = options?.tableName ?? DEFAULT_TABLE;
15
+ const dryRun = options?.dryRun ?? false;
16
+ const count = options?.count;
17
+ await ensureTable(db, tableName);
18
+ const [discovered, applied] = await Promise.all([discover(migrationsDir), getApplied(db, tableName)]);
19
+ const appliedNames = new Set(applied.map((r) => r.name));
20
+ let pending = discovered.filter((m) => !appliedNames.has(dbName(m)));
21
+ if (count !== void 0) {
22
+ pending = pending.slice(0, count);
23
+ }
24
+ if (pending.length === 0) return [];
25
+ const results = [];
26
+ for (const migration of pending) {
27
+ const result = await runUp(db, migration, { dryRun, tableName });
28
+ results.push(result);
29
+ }
30
+ return results;
31
+ }
32
+ async function rollback(db, migrationsDir, options) {
33
+ const tableName = options?.tableName ?? DEFAULT_TABLE;
34
+ const dryRun = options?.dryRun ?? false;
35
+ const count = options?.count ?? 1;
36
+ await ensureTable(db, tableName);
37
+ const [discovered, applied] = await Promise.all([discover(migrationsDir), getApplied(db, tableName)]);
38
+ const migrationsByDbName = new Map(discovered.map((m) => [dbName(m), m]));
39
+ const toRevert = [...applied].reverse().slice(0, count);
40
+ const results = [];
41
+ for (const record of toRevert) {
42
+ const migration = migrationsByDbName.get(record.name);
43
+ if (!migration) {
44
+ throw new Error(`Migration file not found for ${record.name}`);
45
+ }
46
+ const result = await runDown(db, migration, { dryRun, tableName });
47
+ results.push(result);
48
+ }
49
+ return results;
50
+ }
51
+ async function reset(db, migrationsDir, options) {
52
+ const tableName = options?.tableName ?? DEFAULT_TABLE;
53
+ await ensureTable(db, tableName);
54
+ const applied = await getApplied(db, tableName);
55
+ if (applied.length === 0) return [];
56
+ return rollback(db, migrationsDir, {
57
+ count: applied.length,
58
+ dryRun: options?.dryRun,
59
+ tableName
60
+ });
61
+ }
62
+ async function status(db, migrationsDir, options) {
63
+ const tableName = options?.tableName ?? DEFAULT_TABLE;
64
+ await ensureTable(db, tableName);
65
+ const [discovered, applied] = await Promise.all([discover(migrationsDir), getApplied(db, tableName)]);
66
+ const appliedMap = new Map(applied.map((r) => [r.name, r]));
67
+ return discovered.map((m) => {
68
+ const record = appliedMap.get(dbName(m));
69
+ return {
70
+ version: m.version,
71
+ name: m.name,
72
+ filename: m.filename,
73
+ applied: !!record,
74
+ run_on: record?.run_on ?? null
75
+ };
76
+ });
77
+ }
78
+ async function runUp(db, migration, options) {
79
+ const tableName = options?.tableName ?? DEFAULT_TABLE;
80
+ const dryRun = options?.dryRun ?? false;
81
+ if (dryRun) {
82
+ return {
83
+ version: migration.version,
84
+ name: migration.name,
85
+ filename: migration.filename,
86
+ direction: "up",
87
+ durationMs: 0,
88
+ dryRun: true
89
+ };
90
+ }
91
+ const mod = await importMigration(migration.filepath);
92
+ const start = performance.now();
93
+ await db.transaction(async () => {
94
+ await mod.up(db);
95
+ await db.insert(`INSERT INTO ${tableName} (name, run_on) VALUES (?, NOW())`, [dbName(migration)]);
96
+ });
97
+ return {
98
+ version: migration.version,
99
+ name: migration.name,
100
+ filename: migration.filename,
101
+ direction: "up",
102
+ durationMs: Math.round(performance.now() - start),
103
+ dryRun: false
104
+ };
105
+ }
106
+ async function runDown(db, migration, options) {
107
+ const tableName = options?.tableName ?? DEFAULT_TABLE;
108
+ const dryRun = options?.dryRun ?? false;
109
+ if (dryRun) {
110
+ return {
111
+ version: migration.version,
112
+ name: migration.name,
113
+ filename: migration.filename,
114
+ direction: "down",
115
+ durationMs: 0,
116
+ dryRun: true
117
+ };
118
+ }
119
+ const mod = await importMigration(migration.filepath);
120
+ const start = performance.now();
121
+ await db.transaction(async () => {
122
+ await mod.down(db);
123
+ await db.delete(`DELETE FROM ${tableName} WHERE name = ?`, [dbName(migration)]);
124
+ });
125
+ return {
126
+ version: migration.version,
127
+ name: migration.name,
128
+ filename: migration.filename,
129
+ direction: "down",
130
+ durationMs: Math.round(performance.now() - start),
131
+ dryRun: false
132
+ };
133
+ }
134
+ async function scaffold(options) {
135
+ const { name, dir } = options;
136
+ const sqlsDir = join(dir, "sqls");
137
+ await mkdir(sqlsDir, { recursive: true });
138
+ const timestamp = generateTimestamp();
139
+ const baseName = `${timestamp}_${name}`;
140
+ const tsFile = join(dir, `${baseName}.ts`);
141
+ const upSqlFile = join(sqlsDir, `${baseName}-up.sql`);
142
+ const downSqlFile = join(sqlsDir, `${baseName}-down.sql`);
143
+ await Promise.all([writeFile(tsFile, tsTemplate(baseName)), writeFile(upSqlFile, ""), writeFile(downSqlFile, "")]);
144
+ return { ts: tsFile, upSql: upSqlFile, downSql: downSqlFile };
145
+ }
146
+ var DEFAULT_TABLE = "migrations";
147
+ var MIGRATION_PATTERN = /^(\d{14})_(.+)\.[jt]s$/;
148
+ var SAFE_IDENTIFIER = /^[a-zA-Z_]\w*$/;
149
+ function validateTableName(tableName) {
150
+ if (!SAFE_IDENTIFIER.test(tableName)) {
151
+ throw new Error(`Invalid table name: "${tableName}". Must match /^[a-zA-Z_]\\w*$/.`);
152
+ }
153
+ }
154
+ async function ensureTable(db, tableName = DEFAULT_TABLE) {
155
+ validateTableName(tableName);
156
+ await db.statement(`
157
+ CREATE TABLE IF NOT EXISTS ${tableName} (
158
+ id INT NOT NULL AUTO_INCREMENT,
159
+ name VARCHAR(255) NOT NULL,
160
+ run_on DATETIME NOT NULL,
161
+ PRIMARY KEY (id)
162
+ )
163
+ `);
164
+ }
165
+ async function getApplied(db, tableName = DEFAULT_TABLE) {
166
+ validateTableName(tableName);
167
+ return db.select(`SELECT * FROM ${tableName} ORDER BY name ASC`);
168
+ }
169
+ async function discover(migrationsDir) {
170
+ let entries;
171
+ try {
172
+ entries = await readdir(migrationsDir);
173
+ } catch (err) {
174
+ if (err.code === "ENOENT") return [];
175
+ throw err;
176
+ }
177
+ return entries.map((filename) => {
178
+ const match = filename.match(MIGRATION_PATTERN);
179
+ if (!match) return null;
180
+ const [, version, name] = match;
181
+ return { version, name, filename, filepath: join(migrationsDir, filename) };
182
+ }).filter((m) => m !== null).sort((a, b) => a.version.localeCompare(b.version));
183
+ }
184
+ async function importMigration(filepath) {
185
+ const mod = await import(pathToFileURL(filepath).href);
186
+ if (typeof mod.up !== "function" || typeof mod.down !== "function") {
187
+ throw new Error(`Migration ${filepath} must export up(db) and down(db) functions`);
188
+ }
189
+ return mod;
190
+ }
191
+ function dbName(m) {
192
+ return `/${m.version}_${m.name}`;
193
+ }
194
+ function tsTemplate(baseName) {
195
+ return `import type { DbMigrations } from '@maestro-js/db-migrate'
196
+ import { readFile } from 'node:fs/promises'
197
+ import { join, dirname } from 'node:path'
198
+ import { fileURLToPath } from 'node:url'
199
+
200
+ const __dirname = dirname(fileURLToPath(import.meta.url))
201
+
202
+ export async function up(db: DbMigrations.Db) {
203
+ const sql = await readFile(join(__dirname, 'sqls', '${baseName}-up.sql'), 'utf-8')
204
+ await db.statement(sql)
205
+ }
206
+
207
+ export async function down(db: DbMigrations.Db) {
208
+ const sql = await readFile(join(__dirname, 'sqls', '${baseName}-down.sql'), 'utf-8')
209
+ await db.statement(sql)
210
+ }
211
+ `;
212
+ }
213
+ function generateTimestamp() {
214
+ const now = /* @__PURE__ */ new Date();
215
+ const pad = (n) => String(n).padStart(2, "0");
216
+ return String(now.getFullYear()) + pad(now.getMonth() + 1) + pad(now.getDate()) + pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
217
+ }
218
+ export {
219
+ DbMigrations
220
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@maestro-js/db-migrate",
3
+ "description": "Use when working with @maestro-js/db-migrate. Standalone database migration runner with up/down support, dry-run mode, and file scaffolding. Trigger when creating, running, rolling back, or inspecting database migrations. Key capabilities include running pending migrations, rolling back applied migrations, resetting all migrations, checking migration status, scaffolding new migration files with SQL templates, and dry-run previews.",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "devDependencies": {
12
+ "@types/node": "^22.19.11"
13
+ },
14
+ "version": "1.0.0-alpha.0",
15
+ "publishConfig": {
16
+ "access": "restricted"
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "license": "UNLICENSED",
22
+ "engines": {
23
+ "node": ">=22.18.0"
24
+ },
25
+ "repository": "https://github.com/Marcato-Partners/maestro-js",
26
+ "scripts": {
27
+ "build": "tsup --config ../../tsup.config.ts",
28
+ "test": "beartest ./tests/**/*",
29
+ "typecheck": "tsc --noEmit",
30
+ "format": "prettier --write src/ tests/",
31
+ "lint": "prettier --check src/ tests/"
32
+ }
33
+ }