@sebspark/spanner-migrate 0.1.1 → 1.0.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/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli.ts
27
27
  var import_promises2 = __toESM(require("fs/promises"));
28
28
  var import_node_path2 = require("path");
29
- var import_input = __toESM(require("@inquirer/input"));
29
+ var import_prompts = require("@inquirer/prompts");
30
30
  var import_yargs = __toESM(require("yargs"));
31
31
  var import_helpers = require("yargs/helpers");
32
32
 
@@ -70,7 +70,7 @@ var applyDown = async (db) => {
70
70
  json: true
71
71
  };
72
72
  const [rows] = await db.run(req);
73
- const lastMigration = rows == null ? void 0 : rows[0];
73
+ const lastMigration = rows?.[0];
74
74
  if (!lastMigration) {
75
75
  throw new Error("No migrations found to roll back.");
76
76
  }
@@ -102,12 +102,11 @@ var runScript = async (db, script) => {
102
102
  }
103
103
  for (const statement of statements) {
104
104
  console.log(`Executing statement: ${statement}`);
105
- const sql = statement.replace(/--.*$/gm, "");
106
- if (isSchemaChange(sql)) {
107
- await db.updateSchema(sql);
105
+ if (isSchemaChange(statement)) {
106
+ await db.updateSchema(statement);
108
107
  } else {
109
108
  await db.runTransactionAsync(async (transaction) => {
110
- await transaction.runUpdate(sql);
109
+ await transaction.runUpdate(statement);
111
110
  await transaction.commit();
112
111
  });
113
112
  }
@@ -133,12 +132,12 @@ var SQL_CREATE_TABLE_MIGRATIONS = `
133
132
  down STRING(1024)
134
133
  ) PRIMARY KEY (id)
135
134
  `;
136
- var ensureMigrationTable = async (database) => {
137
- const [rows] = await database.run(SQL_SELECT_TABLE_MIGRATIONS);
135
+ var ensureMigrationTable = async (db) => {
136
+ const [rows] = await db.run(SQL_SELECT_TABLE_MIGRATIONS);
138
137
  if (rows.length) return;
139
138
  console.log("Creating migration table");
140
139
  try {
141
- await database.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
140
+ await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
142
141
  } catch (err) {
143
142
  console.error("Failed to create migrations table");
144
143
  throw err;
@@ -169,7 +168,7 @@ var import_node_path = require("path");
169
168
  var getMigrationFiles = async (path) => {
170
169
  try {
171
170
  const files = await (0, import_promises.readdir)(path);
172
- const migrationFileIds = files.filter((file) => file.endsWith(".ts")).map((file) => file.replace(/\.ts$/, ""));
171
+ const migrationFileIds = files.filter((file) => file.endsWith(".sql")).map((file) => file.replace(/\.sql$/, ""));
173
172
  return migrationFileIds;
174
173
  } catch (error) {
175
174
  throw new Error(
@@ -179,35 +178,39 @@ var getMigrationFiles = async (path) => {
179
178
  };
180
179
  var getMigration = async (path, id) => {
181
180
  try {
182
- const filePath = (0, import_node_path.resolve)(process.cwd(), (0, import_node_path.join)(path, `${id}.ts`));
181
+ const filePath = (0, import_node_path.resolve)(process.cwd(), (0, import_node_path.join)(path, `${id}.sql`));
183
182
  try {
184
183
  await (0, import_promises.access)(filePath);
185
184
  } catch (err) {
186
185
  throw new Error(`Migration file not found: ${filePath}`);
187
186
  }
188
- const migrationModule = await import(filePath);
189
- if (!migrationModule.up || !migrationModule.down) {
187
+ const migrationText = await (0, import_promises.readFile)(filePath, "utf8");
188
+ const up2 = getSql(migrationText, "up");
189
+ const down2 = getSql(migrationText, "down");
190
+ const description = getDescription(migrationText);
191
+ if (!up2 || !down2) {
190
192
  throw new Error(
191
193
  `Migration file ${filePath} does not export required scripts (up, down).`
192
194
  );
193
195
  }
194
- return {
195
- id,
196
- description: id.split("_").slice(1).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "),
197
- // Generate a human-readable description
198
- up: migrationModule.up,
199
- down: migrationModule.down
200
- };
196
+ return { id, description, up: up2, down: down2 };
201
197
  } catch (error) {
202
198
  throw new Error(
203
199
  `Failed to get migration ${id}: ${error.message}`
204
200
  );
205
201
  }
206
202
  };
203
+ var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
204
+ var getSql = (text, direction) => {
205
+ const rx = {
206
+ up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
207
+ down: /---- DOWN ----\n([\s\S]*)$/
208
+ };
209
+ return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
210
+ };
207
211
  var getNewMigrations = (applied, files) => {
208
212
  const sortedFiles = files.sort();
209
213
  for (let ix = 0; ix < applied.length; ix++) {
210
- console.log(sortedFiles[ix], applied[ix].id);
211
214
  if (sortedFiles[ix] !== applied[ix].id) {
212
215
  throw new Error(
213
216
  `Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
@@ -215,25 +218,24 @@ var getNewMigrations = (applied, files) => {
215
218
  }
216
219
  }
217
220
  const newMigrations = sortedFiles.slice(applied.length);
218
- console.log(`Found ${newMigrations.length} new migrations.`);
219
221
  return newMigrations;
220
222
  };
221
223
  var createMigration = async (path, description) => {
222
224
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
223
225
  const compactTimestamp = timestamp.replace(/[-:.TZ]/g, "");
224
226
  const parsedDescription = description.replace(/\s+/g, "_").toLowerCase();
225
- const filename = `${compactTimestamp}_${parsedDescription}.ts`;
227
+ const filename = `${compactTimestamp}_${parsedDescription}.sql`;
226
228
  const filePath = (0, import_node_path.join)(path, filename);
227
- const template = `// ${timestamp}
228
- // ${description}
229
+ const template = `-- Created: ${timestamp}
230
+ -- Description: ${description}
231
+
232
+ ---- UP ----
233
+
234
+
235
+
236
+ ---- DOWN ----
229
237
 
230
- export const up = \`
231
- -- SQL for migrate up
232
- \`
233
238
 
234
- export const down = \`
235
- -- SQL for migrate down
236
- \`
237
239
  `;
238
240
  try {
239
241
  await (0, import_promises.mkdir)(path, { recursive: true });
@@ -266,86 +268,116 @@ var init = async (config, configPath) => {
266
268
  var create = async (config, description) => {
267
269
  await createMigration(config.migrationsPath, description);
268
270
  };
269
- var up = async (config, max = 1e3) => {
270
- const db = getDb(config);
271
- await ensureMigrationTable(db);
272
- const appliedMigrations = await getAppliedMigrations(db);
273
- const migrationFiles = await getMigrationFiles(config.migrationsPath);
274
- const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
275
- console.log(`Found ${newMigrations.length} new migrations.`);
276
- console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
277
- for (const id of newMigrations.slice(0, max)) {
278
- const migration = await getMigration(config.migrationsPath, id);
279
- await applyUp(db, migration);
271
+ var up = async (config, database, max) => {
272
+ if (max && !database) {
273
+ throw new Error("Max number of migrations requires specifying a database");
274
+ }
275
+ const databases = database ? [database] : config.instance.databases;
276
+ for (const databaseConfig of databases) {
277
+ const path = {
278
+ projectId: config.projectId,
279
+ instanceName: config.instance.name,
280
+ databaseName: databaseConfig.name
281
+ };
282
+ const db = getDb(path);
283
+ await ensureMigrationTable(db);
284
+ const appliedMigrations = await getAppliedMigrations(db);
285
+ const migrationFiles = await getMigrationFiles(
286
+ databaseConfig.migrationsPath
287
+ );
288
+ const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
289
+ console.log(`Found ${newMigrations.length} new migrations.`);
290
+ console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
291
+ const newMigrationsToApply = max ? newMigrations.slice(0, max) : newMigrations;
292
+ for (const id of newMigrationsToApply) {
293
+ const migration = await getMigration(databaseConfig.migrationsPath, id);
294
+ await applyUp(db, migration);
295
+ }
280
296
  }
281
297
  };
282
- var down = async (config) => {
283
- const db = getDb(config);
298
+ var down = async (config, database) => {
299
+ const path = {
300
+ projectId: config.projectId,
301
+ instanceName: config.instance.name,
302
+ databaseName: database.name
303
+ };
304
+ const db = getDb(path);
284
305
  await ensureMigrationTable(db);
285
306
  await applyDown(db);
286
307
  };
287
- var status = async (config) => {
288
- const db = getDb(config);
289
- await ensureMigrationTable(db);
290
- const appliedMigrations = await getAppliedMigrations(db);
291
- const migrationFiles = await getMigrationFiles(config.migrationsPath);
292
- const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
293
- return [
294
- "Migrations",
295
- "",
296
- "Applied",
297
- "--------------------------------------------------------------------------------",
298
- `${appliedMigrations.map((m) => m.id).join("\n")}
308
+ var status = async (config, databases) => {
309
+ const statuses = [];
310
+ for (const databaseConfig of databases || config.instance.databases) {
311
+ const path = {
312
+ projectId: config.projectId,
313
+ instanceName: config.instance.name,
314
+ databaseName: databaseConfig.name
315
+ };
316
+ const db = getDb(path);
317
+ await ensureMigrationTable(db);
318
+ const appliedMigrations = await getAppliedMigrations(db);
319
+ const migrationFiles = await getMigrationFiles(
320
+ databaseConfig.migrationsPath
321
+ );
322
+ const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
323
+ statuses.push(
324
+ [
325
+ `Migrations [${databaseConfig.name}]`,
326
+ "",
327
+ "Applied",
328
+ "--------------------------------------------------------------------------------",
329
+ `${appliedMigrations.map((m) => m.id).join("\n")}
299
330
  `,
300
- "New",
301
- "--------------------------------------------------------------------------------",
302
- `${newMigrations.join("\n")}
331
+ "New",
332
+ "--------------------------------------------------------------------------------",
333
+ `${newMigrations.join("\n")}
303
334
  `
304
- ].join("\n");
335
+ ].join("\n")
336
+ );
337
+ }
338
+ return statuses.join("\n\n");
305
339
  };
306
340
 
307
341
  // src/cli.ts
308
342
  var CONFIG_FILE = "./.spanner-migrate.config.json";
309
- async function loadConfig() {
310
- try {
311
- const configContent = await import_promises2.default.readFile(CONFIG_FILE, "utf8");
312
- return JSON.parse(configContent);
313
- } catch {
314
- console.error('Config file not found. Run "spanner-migrate init" first.');
315
- process.exit(1);
316
- }
317
- }
318
343
  (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).scriptName("spanner-migrate").usage("$0 <command>").command(
319
344
  "init",
320
345
  "Initialize a .spanner-migrate.config.json file",
321
346
  async () => {
322
- const migrationsPath = await (0, import_input.default)({
323
- message: "Enter the path for your migrations",
324
- required: true,
325
- default: "./migrations"
326
- });
327
- const instanceName = await (0, import_input.default)({
347
+ const instanceName = await (0, import_prompts.input)({
328
348
  message: "Enter Spanner instance name",
329
349
  required: true
330
350
  });
331
- const databaseName = await (0, import_input.default)({
332
- message: "Enter Spanner database name",
333
- required: true
334
- });
335
- const projectId = await (0, import_input.default)({
351
+ const databases = [
352
+ await getDatabaseConfig(true)
353
+ ];
354
+ while (true) {
355
+ const dbConfig = await getDatabaseConfig(false);
356
+ if (!dbConfig) break;
357
+ databases.push(dbConfig);
358
+ }
359
+ const projectId = await (0, import_prompts.input)({
336
360
  message: "Enter Google Cloud project name",
337
361
  required: false
338
362
  });
339
- const config = { instanceName, databaseName, migrationsPath };
363
+ const config = {
364
+ instance: {
365
+ name: instanceName,
366
+ databases
367
+ }
368
+ };
340
369
  if (projectId) config.projectId = projectId;
341
370
  await init(config, CONFIG_FILE);
342
- console.log(`Configuration written to ${CONFIG_FILE}`);
343
371
  }
344
372
  ).command(
345
373
  "create <description ...>",
346
374
  "Create a new migration file",
347
375
  (yargs2) => {
348
- yargs2.positional("description", {
376
+ yargs2.option("database", {
377
+ alias: "d",
378
+ type: "string",
379
+ describe: "Database name"
380
+ }).positional("description", {
349
381
  type: "string",
350
382
  describe: "Description of the migration",
351
383
  demandOption: true
@@ -354,33 +386,157 @@ async function loadConfig() {
354
386
  async (args) => {
355
387
  const config = await loadConfig();
356
388
  const fullDescription = args.description.join(" ");
357
- await create(config, fullDescription);
389
+ let databaseConfig;
390
+ if (args.database) {
391
+ databaseConfig = config.instance.databases.find(
392
+ (db) => db.name === args.database
393
+ );
394
+ if (!databaseConfig) {
395
+ throw new Error(`Unknown database name "${args.database}"`);
396
+ }
397
+ } else {
398
+ if (config.instance.databases.length === 1) {
399
+ databaseConfig = config.instance.databases[0];
400
+ } else {
401
+ databaseConfig = await (0, import_prompts.select)({
402
+ message: "Select database",
403
+ choices: config.instance.databases.map((dbConfig) => ({
404
+ name: dbConfig.name,
405
+ value: dbConfig
406
+ }))
407
+ });
408
+ }
409
+ }
410
+ if (!databaseConfig) throw new Error("No database config found");
411
+ await create(databaseConfig, fullDescription);
358
412
  console.log(
359
- `Migration file created: '${(0, import_node_path2.join)(config.migrationsPath, args.description.join("_"))}.ts'`
413
+ `Migration file created: '${(0, import_node_path2.join)(databaseConfig.migrationsPath, args.description.join("_"))}.sql'`
360
414
  );
361
415
  }
362
416
  ).command(
363
417
  "up",
364
418
  "Apply migrations",
365
419
  (yargs2) => {
366
- yargs2.option("max", {
420
+ yargs2.option("database", {
421
+ alias: "d",
422
+ type: "string",
423
+ describe: "Database name",
424
+ requiresArg: false
425
+ }).option("max", {
367
426
  alias: "m",
368
427
  type: "number",
369
- describe: "Maximum number of migrations to apply",
370
- default: 1e3
428
+ describe: "Maximum number of migrations to apply (requires --database)",
429
+ requiresArg: false
371
430
  });
372
431
  },
373
432
  async (args) => {
374
433
  const config = await loadConfig();
375
- await up(config, args.max);
434
+ if (args.max !== void 0) {
435
+ if (!args.database) {
436
+ throw new Error("The --max option requires a specified --database");
437
+ }
438
+ if (!Number.isInteger(args.max) || args.max <= 0) {
439
+ throw new Error("The --max option must be an integer greater than 0");
440
+ }
441
+ }
442
+ if (args.database) {
443
+ const databaseConfig = config.instance.databases.find(
444
+ (db) => db.name === args.database
445
+ );
446
+ if (!databaseConfig) {
447
+ throw new Error(`Unknown database name "${args.database}"`);
448
+ }
449
+ if (args.max !== void 0) {
450
+ await up(config, databaseConfig, args.max);
451
+ } else {
452
+ await up(config, databaseConfig);
453
+ }
454
+ } else {
455
+ await up(config);
456
+ }
376
457
  console.log("Migrations applied successfully.");
377
458
  }
378
- ).command("down", "Roll back the last applied migration", {}, async () => {
379
- const config = await loadConfig();
380
- await down(config);
381
- console.log("Last migration rolled back successfully.");
382
- }).command("status", "Show the migration status", {}, async () => {
383
- const config = await loadConfig();
384
- const migrationStatus = await status(config);
385
- console.log(migrationStatus);
386
- }).demandCommand().help().parse();
459
+ ).command(
460
+ "down",
461
+ "Roll back the last applied migration",
462
+ (yargs2) => {
463
+ yargs2.option("database", {
464
+ alias: "d",
465
+ type: "string",
466
+ describe: "Specify the database to roll back (required if multiple databases exist)"
467
+ });
468
+ },
469
+ async (args) => {
470
+ const config = await loadConfig();
471
+ let databaseConfig;
472
+ if (args.database) {
473
+ databaseConfig = config.instance.databases.find(
474
+ (dbConfig) => dbConfig.name === args.database
475
+ );
476
+ if (!databaseConfig) {
477
+ throw new Error(`Unknown database name "${args.database}"`);
478
+ }
479
+ } else if (config.instance.databases.length === 1) {
480
+ databaseConfig = config.instance.databases[0];
481
+ } else {
482
+ throw new Error(
483
+ "Multiple databases detected. Use --database to specify which one to roll back."
484
+ );
485
+ }
486
+ if (!databaseConfig) throw new Error("No database config found");
487
+ await down(config, databaseConfig);
488
+ console.log("Last migration rolled back successfully.");
489
+ }
490
+ ).command(
491
+ "status",
492
+ "Show the migration status",
493
+ (yargs2) => {
494
+ yargs2.option("database", {
495
+ alias: "d",
496
+ type: "string",
497
+ describe: "Specify a database to check status (optional, runs on all databases if omitted)"
498
+ });
499
+ },
500
+ async (args) => {
501
+ const config = await loadConfig();
502
+ let migrationStatus;
503
+ if (args.database) {
504
+ const databaseConfig = config.instance.databases.find(
505
+ (db) => db.name === args.database
506
+ );
507
+ if (!databaseConfig) {
508
+ throw new Error(`Unknown database name "${args.database}"`);
509
+ }
510
+ migrationStatus = await status(config, [databaseConfig]);
511
+ } else {
512
+ migrationStatus = await status(config);
513
+ }
514
+ console.log(migrationStatus);
515
+ }
516
+ ).demandCommand().help().parse();
517
+ async function loadConfig() {
518
+ try {
519
+ const configContent = await import_promises2.default.readFile(CONFIG_FILE, "utf8");
520
+ return JSON.parse(configContent);
521
+ } catch {
522
+ console.error('Config file not found. Run "spanner-migrate init" first.');
523
+ process.exit(1);
524
+ }
525
+ }
526
+ var getDatabaseConfig = async (required) => {
527
+ const message = required ? "Enter Spanner database name" : "Enter another Spanner database name [Enter to continue]";
528
+ const name = await (0, import_prompts.input)({
529
+ message,
530
+ required
531
+ });
532
+ if (!name) return;
533
+ const migrationsPath = await (0, import_prompts.input)({
534
+ message: "Enter the path for your migrations",
535
+ required: true,
536
+ default: `./migrations/${name}`
537
+ });
538
+ return {
539
+ name,
540
+ migrationsPath
541
+ };
542
+ };