@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.mjs CHANGED
@@ -5,55 +5,53 @@ import {
5
5
  init,
6
6
  status,
7
7
  up
8
- } from "./chunk-K5WX6ESL.mjs";
8
+ } from "./chunk-SZDH364K.mjs";
9
9
 
10
10
  // src/cli.ts
11
11
  import fs from "node:fs/promises";
12
12
  import { join } from "node:path";
13
- import input from "@inquirer/input";
13
+ import { input, select } from "@inquirer/prompts";
14
14
  import yargs from "yargs";
15
15
  import { hideBin } from "yargs/helpers";
16
16
  var CONFIG_FILE = "./.spanner-migrate.config.json";
17
- async function loadConfig() {
18
- try {
19
- const configContent = await fs.readFile(CONFIG_FILE, "utf8");
20
- return JSON.parse(configContent);
21
- } catch {
22
- console.error('Config file not found. Run "spanner-migrate init" first.');
23
- process.exit(1);
24
- }
25
- }
26
17
  yargs(hideBin(process.argv)).scriptName("spanner-migrate").usage("$0 <command>").command(
27
18
  "init",
28
19
  "Initialize a .spanner-migrate.config.json file",
29
20
  async () => {
30
- const migrationsPath = await input({
31
- message: "Enter the path for your migrations",
32
- required: true,
33
- default: "./migrations"
34
- });
35
21
  const instanceName = await input({
36
22
  message: "Enter Spanner instance name",
37
23
  required: true
38
24
  });
39
- const databaseName = await input({
40
- message: "Enter Spanner database name",
41
- required: true
42
- });
25
+ const databases = [
26
+ await getDatabaseConfig(true)
27
+ ];
28
+ while (true) {
29
+ const dbConfig = await getDatabaseConfig(false);
30
+ if (!dbConfig) break;
31
+ databases.push(dbConfig);
32
+ }
43
33
  const projectId = await input({
44
34
  message: "Enter Google Cloud project name",
45
35
  required: false
46
36
  });
47
- const config = { instanceName, databaseName, migrationsPath };
37
+ const config = {
38
+ instance: {
39
+ name: instanceName,
40
+ databases
41
+ }
42
+ };
48
43
  if (projectId) config.projectId = projectId;
49
44
  await init(config, CONFIG_FILE);
50
- console.log(`Configuration written to ${CONFIG_FILE}`);
51
45
  }
52
46
  ).command(
53
47
  "create <description ...>",
54
48
  "Create a new migration file",
55
49
  (yargs2) => {
56
- yargs2.positional("description", {
50
+ yargs2.option("database", {
51
+ alias: "d",
52
+ type: "string",
53
+ describe: "Database name"
54
+ }).positional("description", {
57
55
  type: "string",
58
56
  describe: "Description of the migration",
59
57
  demandOption: true
@@ -62,33 +60,157 @@ yargs(hideBin(process.argv)).scriptName("spanner-migrate").usage("$0 <command>")
62
60
  async (args) => {
63
61
  const config = await loadConfig();
64
62
  const fullDescription = args.description.join(" ");
65
- await create(config, fullDescription);
63
+ let databaseConfig;
64
+ if (args.database) {
65
+ databaseConfig = config.instance.databases.find(
66
+ (db) => db.name === args.database
67
+ );
68
+ if (!databaseConfig) {
69
+ throw new Error(`Unknown database name "${args.database}"`);
70
+ }
71
+ } else {
72
+ if (config.instance.databases.length === 1) {
73
+ databaseConfig = config.instance.databases[0];
74
+ } else {
75
+ databaseConfig = await select({
76
+ message: "Select database",
77
+ choices: config.instance.databases.map((dbConfig) => ({
78
+ name: dbConfig.name,
79
+ value: dbConfig
80
+ }))
81
+ });
82
+ }
83
+ }
84
+ if (!databaseConfig) throw new Error("No database config found");
85
+ await create(databaseConfig, fullDescription);
66
86
  console.log(
67
- `Migration file created: '${join(config.migrationsPath, args.description.join("_"))}.ts'`
87
+ `Migration file created: '${join(databaseConfig.migrationsPath, args.description.join("_"))}.sql'`
68
88
  );
69
89
  }
70
90
  ).command(
71
91
  "up",
72
92
  "Apply migrations",
73
93
  (yargs2) => {
74
- yargs2.option("max", {
94
+ yargs2.option("database", {
95
+ alias: "d",
96
+ type: "string",
97
+ describe: "Database name",
98
+ requiresArg: false
99
+ }).option("max", {
75
100
  alias: "m",
76
101
  type: "number",
77
- describe: "Maximum number of migrations to apply",
78
- default: 1e3
102
+ describe: "Maximum number of migrations to apply (requires --database)",
103
+ requiresArg: false
79
104
  });
80
105
  },
81
106
  async (args) => {
82
107
  const config = await loadConfig();
83
- await up(config, args.max);
108
+ if (args.max !== void 0) {
109
+ if (!args.database) {
110
+ throw new Error("The --max option requires a specified --database");
111
+ }
112
+ if (!Number.isInteger(args.max) || args.max <= 0) {
113
+ throw new Error("The --max option must be an integer greater than 0");
114
+ }
115
+ }
116
+ if (args.database) {
117
+ const databaseConfig = config.instance.databases.find(
118
+ (db) => db.name === args.database
119
+ );
120
+ if (!databaseConfig) {
121
+ throw new Error(`Unknown database name "${args.database}"`);
122
+ }
123
+ if (args.max !== void 0) {
124
+ await up(config, databaseConfig, args.max);
125
+ } else {
126
+ await up(config, databaseConfig);
127
+ }
128
+ } else {
129
+ await up(config);
130
+ }
84
131
  console.log("Migrations applied successfully.");
85
132
  }
86
- ).command("down", "Roll back the last applied migration", {}, async () => {
87
- const config = await loadConfig();
88
- await down(config);
89
- console.log("Last migration rolled back successfully.");
90
- }).command("status", "Show the migration status", {}, async () => {
91
- const config = await loadConfig();
92
- const migrationStatus = await status(config);
93
- console.log(migrationStatus);
94
- }).demandCommand().help().parse();
133
+ ).command(
134
+ "down",
135
+ "Roll back the last applied migration",
136
+ (yargs2) => {
137
+ yargs2.option("database", {
138
+ alias: "d",
139
+ type: "string",
140
+ describe: "Specify the database to roll back (required if multiple databases exist)"
141
+ });
142
+ },
143
+ async (args) => {
144
+ const config = await loadConfig();
145
+ let databaseConfig;
146
+ if (args.database) {
147
+ databaseConfig = config.instance.databases.find(
148
+ (dbConfig) => dbConfig.name === args.database
149
+ );
150
+ if (!databaseConfig) {
151
+ throw new Error(`Unknown database name "${args.database}"`);
152
+ }
153
+ } else if (config.instance.databases.length === 1) {
154
+ databaseConfig = config.instance.databases[0];
155
+ } else {
156
+ throw new Error(
157
+ "Multiple databases detected. Use --database to specify which one to roll back."
158
+ );
159
+ }
160
+ if (!databaseConfig) throw new Error("No database config found");
161
+ await down(config, databaseConfig);
162
+ console.log("Last migration rolled back successfully.");
163
+ }
164
+ ).command(
165
+ "status",
166
+ "Show the migration status",
167
+ (yargs2) => {
168
+ yargs2.option("database", {
169
+ alias: "d",
170
+ type: "string",
171
+ describe: "Specify a database to check status (optional, runs on all databases if omitted)"
172
+ });
173
+ },
174
+ async (args) => {
175
+ const config = await loadConfig();
176
+ let migrationStatus;
177
+ if (args.database) {
178
+ const databaseConfig = config.instance.databases.find(
179
+ (db) => db.name === args.database
180
+ );
181
+ if (!databaseConfig) {
182
+ throw new Error(`Unknown database name "${args.database}"`);
183
+ }
184
+ migrationStatus = await status(config, [databaseConfig]);
185
+ } else {
186
+ migrationStatus = await status(config);
187
+ }
188
+ console.log(migrationStatus);
189
+ }
190
+ ).demandCommand().help().parse();
191
+ async function loadConfig() {
192
+ try {
193
+ const configContent = await fs.readFile(CONFIG_FILE, "utf8");
194
+ return JSON.parse(configContent);
195
+ } catch {
196
+ console.error('Config file not found. Run "spanner-migrate init" first.');
197
+ process.exit(1);
198
+ }
199
+ }
200
+ var getDatabaseConfig = async (required) => {
201
+ const message = required ? "Enter Spanner database name" : "Enter another Spanner database name [Enter to continue]";
202
+ const name = await input({
203
+ message,
204
+ required
205
+ });
206
+ if (!name) return;
207
+ const migrationsPath = await input({
208
+ message: "Enter the path for your migrations",
209
+ required: true,
210
+ default: `./migrations/${name}`
211
+ });
212
+ return {
213
+ name,
214
+ migrationsPath
215
+ };
216
+ };
package/dist/index.d.mts CHANGED
@@ -1,14 +1,20 @@
1
- type Config = {
1
+ type DatabaseConfig = {
2
+ name: string;
2
3
  migrationsPath: string;
3
- instanceName: string;
4
- databaseName: string;
4
+ };
5
+ type InstanceConfig = {
6
+ name: string;
7
+ databases: DatabaseConfig[];
8
+ };
9
+ type Config = {
10
+ instance: InstanceConfig;
5
11
  projectId?: string;
6
12
  };
7
13
 
8
14
  declare const init: (config: Config, configPath: string) => Promise<void>;
9
- declare const create: (config: Config, description: string) => Promise<void>;
10
- declare const up: (config: Config, max?: number) => Promise<void>;
11
- declare const down: (config: Config) => Promise<void>;
12
- declare const status: (config: Config) => Promise<string>;
15
+ declare const create: (config: DatabaseConfig, description: string) => Promise<void>;
16
+ declare const up: (config: Config, database?: DatabaseConfig, max?: number) => Promise<void>;
17
+ declare const down: (config: Config, database: DatabaseConfig) => Promise<void>;
18
+ declare const status: (config: Config, databases?: DatabaseConfig[]) => Promise<string>;
13
19
 
14
20
  export { create, down, init, status, up };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,20 @@
1
- type Config = {
1
+ type DatabaseConfig = {
2
+ name: string;
2
3
  migrationsPath: string;
3
- instanceName: string;
4
- databaseName: string;
4
+ };
5
+ type InstanceConfig = {
6
+ name: string;
7
+ databases: DatabaseConfig[];
8
+ };
9
+ type Config = {
10
+ instance: InstanceConfig;
5
11
  projectId?: string;
6
12
  };
7
13
 
8
14
  declare const init: (config: Config, configPath: string) => Promise<void>;
9
- declare const create: (config: Config, description: string) => Promise<void>;
10
- declare const up: (config: Config, max?: number) => Promise<void>;
11
- declare const down: (config: Config) => Promise<void>;
12
- declare const status: (config: Config) => Promise<string>;
15
+ declare const create: (config: DatabaseConfig, description: string) => Promise<void>;
16
+ declare const up: (config: Config, database?: DatabaseConfig, max?: number) => Promise<void>;
17
+ declare const down: (config: Config, database: DatabaseConfig) => Promise<void>;
18
+ declare const status: (config: Config, databases?: DatabaseConfig[]) => Promise<string>;
13
19
 
14
20
  export { create, down, init, status, up };
package/dist/index.js CHANGED
@@ -66,7 +66,7 @@ var applyDown = async (db) => {
66
66
  json: true
67
67
  };
68
68
  const [rows] = await db.run(req);
69
- const lastMigration = rows == null ? void 0 : rows[0];
69
+ const lastMigration = rows?.[0];
70
70
  if (!lastMigration) {
71
71
  throw new Error("No migrations found to roll back.");
72
72
  }
@@ -98,12 +98,11 @@ var runScript = async (db, script) => {
98
98
  }
99
99
  for (const statement of statements) {
100
100
  console.log(`Executing statement: ${statement}`);
101
- const sql = statement.replace(/--.*$/gm, "");
102
- if (isSchemaChange(sql)) {
103
- await db.updateSchema(sql);
101
+ if (isSchemaChange(statement)) {
102
+ await db.updateSchema(statement);
104
103
  } else {
105
104
  await db.runTransactionAsync(async (transaction) => {
106
- await transaction.runUpdate(sql);
105
+ await transaction.runUpdate(statement);
107
106
  await transaction.commit();
108
107
  });
109
108
  }
@@ -129,12 +128,12 @@ var SQL_CREATE_TABLE_MIGRATIONS = `
129
128
  down STRING(1024)
130
129
  ) PRIMARY KEY (id)
131
130
  `;
132
- var ensureMigrationTable = async (database) => {
133
- const [rows] = await database.run(SQL_SELECT_TABLE_MIGRATIONS);
131
+ var ensureMigrationTable = async (db) => {
132
+ const [rows] = await db.run(SQL_SELECT_TABLE_MIGRATIONS);
134
133
  if (rows.length) return;
135
134
  console.log("Creating migration table");
136
135
  try {
137
- await database.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
136
+ await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
138
137
  } catch (err) {
139
138
  console.error("Failed to create migrations table");
140
139
  throw err;
@@ -165,7 +164,7 @@ var import_node_path = require("path");
165
164
  var getMigrationFiles = async (path) => {
166
165
  try {
167
166
  const files = await (0, import_promises.readdir)(path);
168
- const migrationFileIds = files.filter((file) => file.endsWith(".ts")).map((file) => file.replace(/\.ts$/, ""));
167
+ const migrationFileIds = files.filter((file) => file.endsWith(".sql")).map((file) => file.replace(/\.sql$/, ""));
169
168
  return migrationFileIds;
170
169
  } catch (error) {
171
170
  throw new Error(
@@ -175,35 +174,39 @@ var getMigrationFiles = async (path) => {
175
174
  };
176
175
  var getMigration = async (path, id) => {
177
176
  try {
178
- const filePath = (0, import_node_path.resolve)(process.cwd(), (0, import_node_path.join)(path, `${id}.ts`));
177
+ const filePath = (0, import_node_path.resolve)(process.cwd(), (0, import_node_path.join)(path, `${id}.sql`));
179
178
  try {
180
179
  await (0, import_promises.access)(filePath);
181
180
  } catch (err) {
182
181
  throw new Error(`Migration file not found: ${filePath}`);
183
182
  }
184
- const migrationModule = await import(filePath);
185
- if (!migrationModule.up || !migrationModule.down) {
183
+ const migrationText = await (0, import_promises.readFile)(filePath, "utf8");
184
+ const up2 = getSql(migrationText, "up");
185
+ const down2 = getSql(migrationText, "down");
186
+ const description = getDescription(migrationText);
187
+ if (!up2 || !down2) {
186
188
  throw new Error(
187
189
  `Migration file ${filePath} does not export required scripts (up, down).`
188
190
  );
189
191
  }
190
- return {
191
- id,
192
- description: id.split("_").slice(1).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "),
193
- // Generate a human-readable description
194
- up: migrationModule.up,
195
- down: migrationModule.down
196
- };
192
+ return { id, description, up: up2, down: down2 };
197
193
  } catch (error) {
198
194
  throw new Error(
199
195
  `Failed to get migration ${id}: ${error.message}`
200
196
  );
201
197
  }
202
198
  };
199
+ var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
200
+ var getSql = (text, direction) => {
201
+ const rx = {
202
+ up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
203
+ down: /---- DOWN ----\n([\s\S]*)$/
204
+ };
205
+ return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
206
+ };
203
207
  var getNewMigrations = (applied, files) => {
204
208
  const sortedFiles = files.sort();
205
209
  for (let ix = 0; ix < applied.length; ix++) {
206
- console.log(sortedFiles[ix], applied[ix].id);
207
210
  if (sortedFiles[ix] !== applied[ix].id) {
208
211
  throw new Error(
209
212
  `Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
@@ -211,25 +214,24 @@ var getNewMigrations = (applied, files) => {
211
214
  }
212
215
  }
213
216
  const newMigrations = sortedFiles.slice(applied.length);
214
- console.log(`Found ${newMigrations.length} new migrations.`);
215
217
  return newMigrations;
216
218
  };
217
219
  var createMigration = async (path, description) => {
218
220
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
219
221
  const compactTimestamp = timestamp.replace(/[-:.TZ]/g, "");
220
222
  const parsedDescription = description.replace(/\s+/g, "_").toLowerCase();
221
- const filename = `${compactTimestamp}_${parsedDescription}.ts`;
223
+ const filename = `${compactTimestamp}_${parsedDescription}.sql`;
222
224
  const filePath = (0, import_node_path.join)(path, filename);
223
- const template = `// ${timestamp}
224
- // ${description}
225
+ const template = `-- Created: ${timestamp}
226
+ -- Description: ${description}
227
+
228
+ ---- UP ----
229
+
230
+
231
+
232
+ ---- DOWN ----
225
233
 
226
- export const up = \`
227
- -- SQL for migrate up
228
- \`
229
234
 
230
- export const down = \`
231
- -- SQL for migrate down
232
- \`
233
235
  `;
234
236
  try {
235
237
  await (0, import_promises.mkdir)(path, { recursive: true });
@@ -262,42 +264,74 @@ var init = async (config, configPath) => {
262
264
  var create = async (config, description) => {
263
265
  await createMigration(config.migrationsPath, description);
264
266
  };
265
- var up = async (config, max = 1e3) => {
266
- const db = getDb(config);
267
- await ensureMigrationTable(db);
268
- const appliedMigrations = await getAppliedMigrations(db);
269
- const migrationFiles = await getMigrationFiles(config.migrationsPath);
270
- const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
271
- console.log(`Found ${newMigrations.length} new migrations.`);
272
- console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
273
- for (const id of newMigrations.slice(0, max)) {
274
- const migration = await getMigration(config.migrationsPath, id);
275
- await applyUp(db, migration);
267
+ var up = async (config, database, max) => {
268
+ if (max && !database) {
269
+ throw new Error("Max number of migrations requires specifying a database");
270
+ }
271
+ const databases = database ? [database] : config.instance.databases;
272
+ for (const databaseConfig of databases) {
273
+ const path = {
274
+ projectId: config.projectId,
275
+ instanceName: config.instance.name,
276
+ databaseName: databaseConfig.name
277
+ };
278
+ const db = getDb(path);
279
+ await ensureMigrationTable(db);
280
+ const appliedMigrations = await getAppliedMigrations(db);
281
+ const migrationFiles = await getMigrationFiles(
282
+ databaseConfig.migrationsPath
283
+ );
284
+ const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
285
+ console.log(`Found ${newMigrations.length} new migrations.`);
286
+ console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
287
+ const newMigrationsToApply = max ? newMigrations.slice(0, max) : newMigrations;
288
+ for (const id of newMigrationsToApply) {
289
+ const migration = await getMigration(databaseConfig.migrationsPath, id);
290
+ await applyUp(db, migration);
291
+ }
276
292
  }
277
293
  };
278
- var down = async (config) => {
279
- const db = getDb(config);
294
+ var down = async (config, database) => {
295
+ const path = {
296
+ projectId: config.projectId,
297
+ instanceName: config.instance.name,
298
+ databaseName: database.name
299
+ };
300
+ const db = getDb(path);
280
301
  await ensureMigrationTable(db);
281
302
  await applyDown(db);
282
303
  };
283
- var status = async (config) => {
284
- const db = getDb(config);
285
- await ensureMigrationTable(db);
286
- const appliedMigrations = await getAppliedMigrations(db);
287
- const migrationFiles = await getMigrationFiles(config.migrationsPath);
288
- const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
289
- return [
290
- "Migrations",
291
- "",
292
- "Applied",
293
- "--------------------------------------------------------------------------------",
294
- `${appliedMigrations.map((m) => m.id).join("\n")}
304
+ var status = async (config, databases) => {
305
+ const statuses = [];
306
+ for (const databaseConfig of databases || config.instance.databases) {
307
+ const path = {
308
+ projectId: config.projectId,
309
+ instanceName: config.instance.name,
310
+ databaseName: databaseConfig.name
311
+ };
312
+ const db = getDb(path);
313
+ await ensureMigrationTable(db);
314
+ const appliedMigrations = await getAppliedMigrations(db);
315
+ const migrationFiles = await getMigrationFiles(
316
+ databaseConfig.migrationsPath
317
+ );
318
+ const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
319
+ statuses.push(
320
+ [
321
+ `Migrations [${databaseConfig.name}]`,
322
+ "",
323
+ "Applied",
324
+ "--------------------------------------------------------------------------------",
325
+ `${appliedMigrations.map((m) => m.id).join("\n")}
295
326
  `,
296
- "New",
297
- "--------------------------------------------------------------------------------",
298
- `${newMigrations.join("\n")}
327
+ "New",
328
+ "--------------------------------------------------------------------------------",
329
+ `${newMigrations.join("\n")}
299
330
  `
300
- ].join("\n");
331
+ ].join("\n")
332
+ );
333
+ }
334
+ return statuses.join("\n\n");
301
335
  };
302
336
  // Annotate the CommonJS export names for ESM import in node:
303
337
  0 && (module.exports = {
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  init,
5
5
  status,
6
6
  up
7
- } from "./chunk-K5WX6ESL.mjs";
7
+ } from "./chunk-SZDH364K.mjs";
8
8
  export {
9
9
  create,
10
10
  down,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sebspark/spanner-migrate",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -14,24 +14,29 @@
14
14
  "dev": "tsc --watch --noEmit",
15
15
  "lint": "biome check .",
16
16
  "test": "jest --config jest.config.ts --passWithNoTests --coverage",
17
- "test:e2e": "jest --config jest.e2e.config.ts",
17
+ "test:e2e": "jest --config jest.e2e.config.ts --runInBand",
18
18
  "typecheck": "tsc --noEmit "
19
19
  },
20
20
  "devDependencies": {
21
- "@google-cloud/spanner": "^7.17.1",
21
+ "@google-cloud/spanner": "7.18.1",
22
+ "@sebspark/cli-tester": "*",
22
23
  "@sebspark/spanner-mock": "*",
23
- "@types/jest": "^29.5.14",
24
- "@types/yargs": "^17.0.33",
25
- "jest": "^29.7.0",
26
- "testcontainers": "^10.16.0",
27
- "ts-jest": "^29.2.5",
24
+ "@types/jest": "29.5.14",
25
+ "@types/yargs": "17.0.33",
26
+ "jest": "29.7.0",
27
+ "testcontainers": "10.18.0",
28
+ "ts-jest": "29.2.6",
28
29
  "tsconfig": "*"
29
30
  },
30
31
  "peerDependencies": {
31
32
  "@google-cloud/spanner": "*"
32
33
  },
33
34
  "dependencies": {
34
- "@inquirer/input": "4.1.3",
35
+ "@inquirer/prompts": "7.3.2",
36
+ "@jest/globals": "29.7.0",
37
+ "@types/node": "20.17.17",
38
+ "typescript": "5.7.3",
39
+ "vitest": "3.0.7",
35
40
  "yargs": "17.7.2"
36
41
  }
37
42
  }