@sebspark/spanner-migrate 1.1.7 → 2.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 +20 -383
- package/dist/index.js +9 -375
- package/package.json +5 -4
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -216
- package/dist/index.d.mts +0 -20
- package/dist/index.mjs +0 -14
- /package/dist/{chunk-YL5IK6VK.mjs → chunk-XP6DPC5R.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,387 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
for (let key of __getOwnPropNames(from))
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
-
}
|
|
15
|
-
return to;
|
|
16
|
-
};
|
|
17
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
-
mod
|
|
24
|
-
));
|
|
25
|
-
|
|
26
|
-
// src/cli.ts
|
|
27
|
-
var import_promises2 = __toESM(require("fs/promises"));
|
|
28
|
-
var import_node_path2 = require("path");
|
|
29
|
-
var import_prompts = require("@inquirer/prompts");
|
|
30
|
-
var import_yargs = __toESM(require("yargs"));
|
|
31
|
-
var import_helpers = require("yargs/helpers");
|
|
32
|
-
|
|
33
|
-
// src/index.ts
|
|
34
|
-
var import_spanner = require("@google-cloud/spanner");
|
|
35
|
-
|
|
36
|
-
// src/apply.ts
|
|
37
|
-
var applyUp = async (db, migration) => {
|
|
38
|
-
try {
|
|
39
|
-
await runScript(db, migration.up);
|
|
40
|
-
console.log(
|
|
41
|
-
`Successfully applied migration script for: ${migration.description}`
|
|
42
|
-
);
|
|
43
|
-
const req = {
|
|
44
|
-
sql: `
|
|
45
|
-
INSERT INTO migrations (id, description, applied_at, up, down)
|
|
46
|
-
VALUES (@id, @description, CURRENT_TIMESTAMP(), @up, @down)
|
|
47
|
-
`,
|
|
48
|
-
params: migration,
|
|
49
|
-
json: true
|
|
50
|
-
};
|
|
51
|
-
await db.runTransactionAsync(async (transaction) => {
|
|
52
|
-
await transaction.runUpdate(req);
|
|
53
|
-
await transaction.commit();
|
|
54
|
-
});
|
|
55
|
-
console.log(`Migration recorded in the database: ${migration.id}`);
|
|
56
|
-
} catch (error) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Failed to apply migration ${migration.id}: ${error.message}`
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
var applyDown = async (db) => {
|
|
63
|
-
const req = {
|
|
64
|
-
sql: `
|
|
65
|
-
SELECT id, description, up, down
|
|
66
|
-
FROM migrations
|
|
67
|
-
ORDER BY applied_at DESC
|
|
68
|
-
LIMIT 1
|
|
69
|
-
`,
|
|
70
|
-
json: true
|
|
71
|
-
};
|
|
72
|
-
const [rows] = await db.run(req);
|
|
73
|
-
const lastMigration = rows?.[0];
|
|
74
|
-
if (!lastMigration) {
|
|
75
|
-
throw new Error("No migrations found to roll back.");
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
await runScript(db, lastMigration.down);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Failed to apply down script for migration ${lastMigration.id}: ${error.message}`
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
await db.runTransactionAsync(async (transaction) => {
|
|
85
|
-
await transaction.runUpdate({
|
|
86
|
-
sql: `
|
|
87
|
-
DELETE FROM migrations
|
|
88
|
-
WHERE id = @id
|
|
89
|
-
`,
|
|
90
|
-
params: { id: lastMigration.id }
|
|
91
|
-
});
|
|
92
|
-
await transaction.commit();
|
|
93
|
-
});
|
|
94
|
-
console.log(
|
|
95
|
-
`Successfully rolled back migration: ${lastMigration.description}`
|
|
96
|
-
);
|
|
97
|
-
};
|
|
98
|
-
var runScript = async (db, script) => {
|
|
99
|
-
const statements = script.split(";").filter(Boolean).map((stmt) => stmt.trim()).filter(Boolean);
|
|
100
|
-
if (statements.length === 0) {
|
|
101
|
-
throw new Error("No valid SQL statements found in the script.");
|
|
102
|
-
}
|
|
103
|
-
for (const statement of statements) {
|
|
104
|
-
console.log(`Executing statement: ${statement}`);
|
|
105
|
-
if (isSchemaChange(statement)) {
|
|
106
|
-
await db.updateSchema(statement);
|
|
107
|
-
} else {
|
|
108
|
-
await db.runTransactionAsync(async (transaction) => {
|
|
109
|
-
await transaction.runUpdate(statement);
|
|
110
|
-
await transaction.commit();
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
var isSchemaChange = (sql) => /^\s*(CREATE|ALTER|DROP|TRUNCATE)\b/i.test(sql);
|
|
116
|
-
|
|
117
|
-
// src/db.ts
|
|
118
|
-
var SQL_SELECT_TABLE_MIGRATIONS = `
|
|
119
|
-
SELECT
|
|
120
|
-
t.TABLE_NAME,
|
|
121
|
-
c.COLUMN_NAME,
|
|
122
|
-
c.SPANNER_TYPE
|
|
123
|
-
FROM
|
|
124
|
-
information_schema.TABLES t
|
|
125
|
-
INNER JOIN
|
|
126
|
-
information_schema.COLUMNS c
|
|
127
|
-
ON t.TABLE_NAME = c.TABLE_NAME
|
|
128
|
-
WHERE
|
|
129
|
-
t.TABLE_NAME = 'migrations'
|
|
130
|
-
`;
|
|
131
|
-
var SQL_CREATE_TABLE_MIGRATIONS = `
|
|
132
|
-
CREATE TABLE migrations (
|
|
133
|
-
id STRING(128) NOT NULL,
|
|
134
|
-
description STRING(256) NOT NULL,
|
|
135
|
-
applied_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp = true),
|
|
136
|
-
up STRING(MAX),
|
|
137
|
-
down STRING(MAX)
|
|
138
|
-
) PRIMARY KEY (id)
|
|
139
|
-
`;
|
|
140
|
-
var ensureMigrationTable = async (db) => {
|
|
141
|
-
const [rows] = await db.run({ sql: SQL_SELECT_TABLE_MIGRATIONS, json: true });
|
|
142
|
-
if (rows.length === 0) {
|
|
143
|
-
console.log("Creating migration table");
|
|
144
|
-
try {
|
|
145
|
-
await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error("Failed to create migrations table");
|
|
148
|
-
throw err;
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
const typedRows = rows;
|
|
152
|
-
const upType = typedRows.find((r) => r.COLUMN_NAME === "up");
|
|
153
|
-
const downType = typedRows.find((r) => r.COLUMN_NAME === "down");
|
|
154
|
-
const expectedType = "STRING(MAX)";
|
|
155
|
-
if (upType?.SPANNER_TYPE !== expectedType) {
|
|
156
|
-
try {
|
|
157
|
-
console.log(
|
|
158
|
-
`Updating 'up' column of migration table to ${expectedType}`
|
|
159
|
-
);
|
|
160
|
-
await db.updateSchema(
|
|
161
|
-
`ALTER TABLE migrations ALTER COLUMN up ${expectedType}`
|
|
162
|
-
);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error("Failed to update migrations table");
|
|
165
|
-
throw err;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (downType?.SPANNER_TYPE !== expectedType) {
|
|
169
|
-
try {
|
|
170
|
-
console.log(
|
|
171
|
-
`Updating 'down' column of migration table to ${expectedType}`
|
|
172
|
-
);
|
|
173
|
-
await db.updateSchema(
|
|
174
|
-
`ALTER TABLE migrations ALTER COLUMN down ${expectedType}`
|
|
175
|
-
);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
console.error("Failed to update migrations table");
|
|
178
|
-
throw err;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
var getAppliedMigrations = async (db) => {
|
|
184
|
-
try {
|
|
185
|
-
const req = {
|
|
186
|
-
sql: `
|
|
187
|
-
SELECT id, description, up, down, applied_at as appliedAt
|
|
188
|
-
FROM migrations
|
|
189
|
-
ORDER BY applied_at ASC
|
|
190
|
-
`,
|
|
191
|
-
json: true
|
|
192
|
-
};
|
|
193
|
-
const [rows] = await db.run(req);
|
|
194
|
-
return rows;
|
|
195
|
-
} catch (error) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
`Failed to get applied migrations: ${error.message}`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// src/files.ts
|
|
203
|
-
var import_promises = require("fs/promises");
|
|
204
|
-
var import_node_path = require("path");
|
|
205
|
-
var getMigrationFiles = async (path) => {
|
|
206
|
-
try {
|
|
207
|
-
const files = await (0, import_promises.readdir)(path);
|
|
208
|
-
const migrationFileIds = files.filter((file) => file.endsWith(".sql")).map((file) => file.replace(/\.sql$/, ""));
|
|
209
|
-
return migrationFileIds;
|
|
210
|
-
} catch (error) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
`Failed to get migration files: ${error.message}`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
var getMigration = async (path, id) => {
|
|
217
|
-
try {
|
|
218
|
-
const filePath = (0, import_node_path.resolve)(process.cwd(), (0, import_node_path.join)(path, `${id}.sql`));
|
|
219
|
-
try {
|
|
220
|
-
await (0, import_promises.access)(filePath);
|
|
221
|
-
} catch {
|
|
222
|
-
throw new Error(`Migration file not found: ${filePath}`);
|
|
223
|
-
}
|
|
224
|
-
const migrationText = await (0, import_promises.readFile)(filePath, "utf8");
|
|
225
|
-
const up2 = getSql(migrationText, "up");
|
|
226
|
-
const down2 = getSql(migrationText, "down");
|
|
227
|
-
const description = getDescription(migrationText);
|
|
228
|
-
if (!up2 || !down2) {
|
|
229
|
-
throw new Error(
|
|
230
|
-
`Migration file ${filePath} does not export required scripts (up, down).`
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
return { id, description, up: up2, down: down2 };
|
|
234
|
-
} catch (error) {
|
|
235
|
-
throw new Error(
|
|
236
|
-
`Failed to get migration ${id}: ${error.message}`
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
241
|
-
var getSql = (text, direction) => {
|
|
242
|
-
const rx = {
|
|
243
|
-
up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
|
|
244
|
-
down: /---- DOWN ----\n([\s\S]*)$/
|
|
245
|
-
};
|
|
246
|
-
return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
|
|
247
|
-
};
|
|
248
|
-
var getNewMigrations = (applied, files) => {
|
|
249
|
-
const sortedFiles = files.sort();
|
|
250
|
-
for (let ix = 0; ix < applied.length; ix++) {
|
|
251
|
-
if (sortedFiles[ix] !== applied[ix].id) {
|
|
252
|
-
throw new Error(
|
|
253
|
-
`Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
const newMigrations = sortedFiles.slice(applied.length);
|
|
258
|
-
return newMigrations;
|
|
259
|
-
};
|
|
260
|
-
var createMigration = async (path, description) => {
|
|
261
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
262
|
-
const compactTimestamp = timestamp.replace(/[-:.TZ]/g, "");
|
|
263
|
-
const parsedDescription = description.replace(/\s+/g, "_").toLowerCase();
|
|
264
|
-
const filename = `${compactTimestamp}_${parsedDescription}.sql`;
|
|
265
|
-
const filePath = (0, import_node_path.join)(path, filename);
|
|
266
|
-
const template = `-- Created: ${timestamp}
|
|
267
|
-
-- Description: ${description}
|
|
268
|
-
|
|
269
|
-
---- UP ----
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
---- DOWN ----
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
`;
|
|
277
|
-
try {
|
|
278
|
-
await (0, import_promises.mkdir)(path, { recursive: true });
|
|
279
|
-
await (0, import_promises.writeFile)(filePath, template.trim(), "utf8");
|
|
280
|
-
console.log(`Migration created: ${filePath}`);
|
|
281
|
-
} catch (error) {
|
|
282
|
-
throw new Error(`Error creating migration: ${error.message}`);
|
|
283
|
-
}
|
|
284
|
-
};
|
|
285
|
-
var writeConfig = async (path, config) => {
|
|
286
|
-
try {
|
|
287
|
-
const configContent = JSON.stringify(config, null, 2);
|
|
288
|
-
await (0, import_promises.writeFile)(path, configContent, "utf8");
|
|
289
|
-
console.log(`Configuration written to ${path}`);
|
|
290
|
-
} catch (error) {
|
|
291
|
-
throw new Error(
|
|
292
|
-
`Error writing configuration to ${path}: ${error.message}`
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
// src/index.ts
|
|
298
|
-
var getDb = ({ projectId, databaseName, instanceName }) => {
|
|
299
|
-
const spanner = projectId ? new import_spanner.Spanner({ projectId }) : new import_spanner.Spanner();
|
|
300
|
-
return spanner.instance(instanceName).database(databaseName);
|
|
301
|
-
};
|
|
302
|
-
var init = async (config, configPath) => {
|
|
303
|
-
await writeConfig(configPath, config);
|
|
304
|
-
};
|
|
305
|
-
var create = async (config, description) => {
|
|
306
|
-
await createMigration(config.migrationsPath, description);
|
|
307
|
-
};
|
|
308
|
-
var up = async (config, database, max) => {
|
|
309
|
-
if (max && !database) {
|
|
310
|
-
throw new Error("Max number of migrations requires specifying a database");
|
|
311
|
-
}
|
|
312
|
-
const databases = database ? [database] : config.instance.databases;
|
|
313
|
-
for (const databaseConfig of databases) {
|
|
314
|
-
const path = {
|
|
315
|
-
projectId: config.projectId,
|
|
316
|
-
instanceName: config.instance.name,
|
|
317
|
-
databaseName: databaseConfig.name
|
|
318
|
-
};
|
|
319
|
-
const db = getDb(path);
|
|
320
|
-
await ensureMigrationTable(db);
|
|
321
|
-
const appliedMigrations = await getAppliedMigrations(db);
|
|
322
|
-
const migrationFiles = await getMigrationFiles(
|
|
323
|
-
databaseConfig.migrationsPath
|
|
324
|
-
);
|
|
325
|
-
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
326
|
-
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
327
|
-
console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
|
|
328
|
-
const newMigrationsToApply = max ? newMigrations.slice(0, max) : newMigrations;
|
|
329
|
-
for (const id of newMigrationsToApply) {
|
|
330
|
-
const migration = await getMigration(databaseConfig.migrationsPath, id);
|
|
331
|
-
await applyUp(db, migration);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
var down = async (config, database) => {
|
|
336
|
-
const path = {
|
|
337
|
-
projectId: config.projectId,
|
|
338
|
-
instanceName: config.instance.name,
|
|
339
|
-
databaseName: database.name
|
|
340
|
-
};
|
|
341
|
-
const db = getDb(path);
|
|
342
|
-
await ensureMigrationTable(db);
|
|
343
|
-
await applyDown(db);
|
|
344
|
-
};
|
|
345
|
-
var status = async (config, databases) => {
|
|
346
|
-
const statuses = [];
|
|
347
|
-
for (const databaseConfig of databases || config.instance.databases) {
|
|
348
|
-
const path = {
|
|
349
|
-
projectId: config.projectId,
|
|
350
|
-
instanceName: config.instance.name,
|
|
351
|
-
databaseName: databaseConfig.name
|
|
352
|
-
};
|
|
353
|
-
const db = getDb(path);
|
|
354
|
-
await ensureMigrationTable(db);
|
|
355
|
-
const appliedMigrations = await getAppliedMigrations(db);
|
|
356
|
-
const migrationFiles = await getMigrationFiles(
|
|
357
|
-
databaseConfig.migrationsPath
|
|
358
|
-
);
|
|
359
|
-
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
360
|
-
statuses.push(
|
|
361
|
-
[
|
|
362
|
-
`Migrations [${databaseConfig.name}]`,
|
|
363
|
-
"",
|
|
364
|
-
"Applied",
|
|
365
|
-
"--------------------------------------------------------------------------------",
|
|
366
|
-
`${appliedMigrations.map((m) => m.id).join("\n")}
|
|
367
|
-
`,
|
|
368
|
-
"New",
|
|
369
|
-
"--------------------------------------------------------------------------------",
|
|
370
|
-
`${newMigrations.join("\n")}
|
|
371
|
-
`
|
|
372
|
-
].join("\n")
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
return statuses.join("\n\n");
|
|
376
|
-
};
|
|
2
|
+
import {
|
|
3
|
+
create,
|
|
4
|
+
down,
|
|
5
|
+
init,
|
|
6
|
+
status,
|
|
7
|
+
up
|
|
8
|
+
} from "./chunk-XP6DPC5R.js";
|
|
377
9
|
|
|
378
10
|
// src/cli.ts
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { input, select } from "@inquirer/prompts";
|
|
14
|
+
import yargs from "yargs";
|
|
15
|
+
import { hideBin } from "yargs/helpers";
|
|
379
16
|
var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
380
|
-
(
|
|
17
|
+
yargs(hideBin(process.argv)).scriptName("spanner-migrate").usage("$0 <command>").command(
|
|
381
18
|
"init",
|
|
382
19
|
"Initialize a .spanner-migrate.config.json file",
|
|
383
20
|
async () => {
|
|
384
|
-
const instanceName = await
|
|
21
|
+
const instanceName = await input({
|
|
385
22
|
message: "Enter Spanner instance name",
|
|
386
23
|
required: true
|
|
387
24
|
});
|
|
@@ -393,7 +30,7 @@ var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
|
393
30
|
if (!dbConfig) break;
|
|
394
31
|
databases.push(dbConfig);
|
|
395
32
|
}
|
|
396
|
-
const projectId = await
|
|
33
|
+
const projectId = await input({
|
|
397
34
|
message: "Enter Google Cloud project name",
|
|
398
35
|
required: false
|
|
399
36
|
});
|
|
@@ -435,7 +72,7 @@ var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
|
435
72
|
if (config.instance.databases.length === 1) {
|
|
436
73
|
databaseConfig = config.instance.databases[0];
|
|
437
74
|
} else {
|
|
438
|
-
databaseConfig = await
|
|
75
|
+
databaseConfig = await select({
|
|
439
76
|
message: "Select database",
|
|
440
77
|
choices: config.instance.databases.map((dbConfig) => ({
|
|
441
78
|
name: dbConfig.name,
|
|
@@ -447,7 +84,7 @@ var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
|
447
84
|
if (!databaseConfig) throw new Error("No database config found");
|
|
448
85
|
await create(databaseConfig, fullDescription);
|
|
449
86
|
console.log(
|
|
450
|
-
`Migration file created: '${
|
|
87
|
+
`Migration file created: '${join(databaseConfig.migrationsPath, args.description.join("_"))}.sql'`
|
|
451
88
|
);
|
|
452
89
|
}
|
|
453
90
|
).command(
|
|
@@ -553,7 +190,7 @@ var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
|
553
190
|
).demandCommand().help().parse();
|
|
554
191
|
async function loadConfig() {
|
|
555
192
|
try {
|
|
556
|
-
const configContent = await
|
|
193
|
+
const configContent = await fs.readFile(CONFIG_FILE, "utf8");
|
|
557
194
|
return JSON.parse(configContent);
|
|
558
195
|
} catch {
|
|
559
196
|
console.error('Config file not found. Run "spanner-migrate init" first.');
|
|
@@ -562,12 +199,12 @@ async function loadConfig() {
|
|
|
562
199
|
}
|
|
563
200
|
var getDatabaseConfig = async (required) => {
|
|
564
201
|
const message = required ? "Enter Spanner database name" : "Enter another Spanner database name [Enter to continue]";
|
|
565
|
-
const name = await
|
|
202
|
+
const name = await input({
|
|
566
203
|
message,
|
|
567
204
|
required
|
|
568
205
|
});
|
|
569
206
|
if (!name) return;
|
|
570
|
-
const migrationsPath = await
|
|
207
|
+
const migrationsPath = await input({
|
|
571
208
|
message: "Enter the path for your migrations",
|
|
572
209
|
required: true,
|
|
573
210
|
default: `./migrations/${name}`
|
package/dist/index.js
CHANGED
|
@@ -1,380 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
-
var __export = (target, all) => {
|
|
7
|
-
for (var name in all)
|
|
8
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
-
};
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
-
for (let key of __getOwnPropNames(from))
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
-
|
|
20
|
-
// src/index.ts
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
create: () => create,
|
|
24
|
-
down: () => down,
|
|
25
|
-
init: () => init,
|
|
26
|
-
status: () => status,
|
|
27
|
-
up: () => up
|
|
28
|
-
});
|
|
29
|
-
module.exports = __toCommonJS(index_exports);
|
|
30
|
-
var import_spanner = require("@google-cloud/spanner");
|
|
31
|
-
|
|
32
|
-
// src/apply.ts
|
|
33
|
-
var applyUp = async (db, migration) => {
|
|
34
|
-
try {
|
|
35
|
-
await runScript(db, migration.up);
|
|
36
|
-
console.log(
|
|
37
|
-
`Successfully applied migration script for: ${migration.description}`
|
|
38
|
-
);
|
|
39
|
-
const req = {
|
|
40
|
-
sql: `
|
|
41
|
-
INSERT INTO migrations (id, description, applied_at, up, down)
|
|
42
|
-
VALUES (@id, @description, CURRENT_TIMESTAMP(), @up, @down)
|
|
43
|
-
`,
|
|
44
|
-
params: migration,
|
|
45
|
-
json: true
|
|
46
|
-
};
|
|
47
|
-
await db.runTransactionAsync(async (transaction) => {
|
|
48
|
-
await transaction.runUpdate(req);
|
|
49
|
-
await transaction.commit();
|
|
50
|
-
});
|
|
51
|
-
console.log(`Migration recorded in the database: ${migration.id}`);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Failed to apply migration ${migration.id}: ${error.message}`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
var applyDown = async (db) => {
|
|
59
|
-
const req = {
|
|
60
|
-
sql: `
|
|
61
|
-
SELECT id, description, up, down
|
|
62
|
-
FROM migrations
|
|
63
|
-
ORDER BY applied_at DESC
|
|
64
|
-
LIMIT 1
|
|
65
|
-
`,
|
|
66
|
-
json: true
|
|
67
|
-
};
|
|
68
|
-
const [rows] = await db.run(req);
|
|
69
|
-
const lastMigration = rows?.[0];
|
|
70
|
-
if (!lastMigration) {
|
|
71
|
-
throw new Error("No migrations found to roll back.");
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
await runScript(db, lastMigration.down);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Failed to apply down script for migration ${lastMigration.id}: ${error.message}`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
await db.runTransactionAsync(async (transaction) => {
|
|
81
|
-
await transaction.runUpdate({
|
|
82
|
-
sql: `
|
|
83
|
-
DELETE FROM migrations
|
|
84
|
-
WHERE id = @id
|
|
85
|
-
`,
|
|
86
|
-
params: { id: lastMigration.id }
|
|
87
|
-
});
|
|
88
|
-
await transaction.commit();
|
|
89
|
-
});
|
|
90
|
-
console.log(
|
|
91
|
-
`Successfully rolled back migration: ${lastMigration.description}`
|
|
92
|
-
);
|
|
93
|
-
};
|
|
94
|
-
var runScript = async (db, script) => {
|
|
95
|
-
const statements = script.split(";").filter(Boolean).map((stmt) => stmt.trim()).filter(Boolean);
|
|
96
|
-
if (statements.length === 0) {
|
|
97
|
-
throw new Error("No valid SQL statements found in the script.");
|
|
98
|
-
}
|
|
99
|
-
for (const statement of statements) {
|
|
100
|
-
console.log(`Executing statement: ${statement}`);
|
|
101
|
-
if (isSchemaChange(statement)) {
|
|
102
|
-
await db.updateSchema(statement);
|
|
103
|
-
} else {
|
|
104
|
-
await db.runTransactionAsync(async (transaction) => {
|
|
105
|
-
await transaction.runUpdate(statement);
|
|
106
|
-
await transaction.commit();
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
var isSchemaChange = (sql) => /^\s*(CREATE|ALTER|DROP|TRUNCATE)\b/i.test(sql);
|
|
112
|
-
|
|
113
|
-
// src/db.ts
|
|
114
|
-
var SQL_SELECT_TABLE_MIGRATIONS = `
|
|
115
|
-
SELECT
|
|
116
|
-
t.TABLE_NAME,
|
|
117
|
-
c.COLUMN_NAME,
|
|
118
|
-
c.SPANNER_TYPE
|
|
119
|
-
FROM
|
|
120
|
-
information_schema.TABLES t
|
|
121
|
-
INNER JOIN
|
|
122
|
-
information_schema.COLUMNS c
|
|
123
|
-
ON t.TABLE_NAME = c.TABLE_NAME
|
|
124
|
-
WHERE
|
|
125
|
-
t.TABLE_NAME = 'migrations'
|
|
126
|
-
`;
|
|
127
|
-
var SQL_CREATE_TABLE_MIGRATIONS = `
|
|
128
|
-
CREATE TABLE migrations (
|
|
129
|
-
id STRING(128) NOT NULL,
|
|
130
|
-
description STRING(256) NOT NULL,
|
|
131
|
-
applied_at TIMESTAMP NOT NULL OPTIONS (allow_commit_timestamp = true),
|
|
132
|
-
up STRING(MAX),
|
|
133
|
-
down STRING(MAX)
|
|
134
|
-
) PRIMARY KEY (id)
|
|
135
|
-
`;
|
|
136
|
-
var ensureMigrationTable = async (db) => {
|
|
137
|
-
const [rows] = await db.run({ sql: SQL_SELECT_TABLE_MIGRATIONS, json: true });
|
|
138
|
-
if (rows.length === 0) {
|
|
139
|
-
console.log("Creating migration table");
|
|
140
|
-
try {
|
|
141
|
-
await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
|
|
142
|
-
} catch (err) {
|
|
143
|
-
console.error("Failed to create migrations table");
|
|
144
|
-
throw err;
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
const typedRows = rows;
|
|
148
|
-
const upType = typedRows.find((r) => r.COLUMN_NAME === "up");
|
|
149
|
-
const downType = typedRows.find((r) => r.COLUMN_NAME === "down");
|
|
150
|
-
const expectedType = "STRING(MAX)";
|
|
151
|
-
if (upType?.SPANNER_TYPE !== expectedType) {
|
|
152
|
-
try {
|
|
153
|
-
console.log(
|
|
154
|
-
`Updating 'up' column of migration table to ${expectedType}`
|
|
155
|
-
);
|
|
156
|
-
await db.updateSchema(
|
|
157
|
-
`ALTER TABLE migrations ALTER COLUMN up ${expectedType}`
|
|
158
|
-
);
|
|
159
|
-
} catch (err) {
|
|
160
|
-
console.error("Failed to update migrations table");
|
|
161
|
-
throw err;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (downType?.SPANNER_TYPE !== expectedType) {
|
|
165
|
-
try {
|
|
166
|
-
console.log(
|
|
167
|
-
`Updating 'down' column of migration table to ${expectedType}`
|
|
168
|
-
);
|
|
169
|
-
await db.updateSchema(
|
|
170
|
-
`ALTER TABLE migrations ALTER COLUMN down ${expectedType}`
|
|
171
|
-
);
|
|
172
|
-
} catch (err) {
|
|
173
|
-
console.error("Failed to update migrations table");
|
|
174
|
-
throw err;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
var getAppliedMigrations = async (db) => {
|
|
180
|
-
try {
|
|
181
|
-
const req = {
|
|
182
|
-
sql: `
|
|
183
|
-
SELECT id, description, up, down, applied_at as appliedAt
|
|
184
|
-
FROM migrations
|
|
185
|
-
ORDER BY applied_at ASC
|
|
186
|
-
`,
|
|
187
|
-
json: true
|
|
188
|
-
};
|
|
189
|
-
const [rows] = await db.run(req);
|
|
190
|
-
return rows;
|
|
191
|
-
} catch (error) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`Failed to get applied migrations: ${error.message}`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
// src/files.ts
|
|
199
|
-
var import_promises = require("fs/promises");
|
|
200
|
-
var import_node_path = require("path");
|
|
201
|
-
var getMigrationFiles = async (path) => {
|
|
202
|
-
try {
|
|
203
|
-
const files = await (0, import_promises.readdir)(path);
|
|
204
|
-
const migrationFileIds = files.filter((file) => file.endsWith(".sql")).map((file) => file.replace(/\.sql$/, ""));
|
|
205
|
-
return migrationFileIds;
|
|
206
|
-
} catch (error) {
|
|
207
|
-
throw new Error(
|
|
208
|
-
`Failed to get migration files: ${error.message}`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
var getMigration = async (path, id) => {
|
|
213
|
-
try {
|
|
214
|
-
const filePath = (0, import_node_path.resolve)(process.cwd(), (0, import_node_path.join)(path, `${id}.sql`));
|
|
215
|
-
try {
|
|
216
|
-
await (0, import_promises.access)(filePath);
|
|
217
|
-
} catch {
|
|
218
|
-
throw new Error(`Migration file not found: ${filePath}`);
|
|
219
|
-
}
|
|
220
|
-
const migrationText = await (0, import_promises.readFile)(filePath, "utf8");
|
|
221
|
-
const up2 = getSql(migrationText, "up");
|
|
222
|
-
const down2 = getSql(migrationText, "down");
|
|
223
|
-
const description = getDescription(migrationText);
|
|
224
|
-
if (!up2 || !down2) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`Migration file ${filePath} does not export required scripts (up, down).`
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
return { id, description, up: up2, down: down2 };
|
|
230
|
-
} catch (error) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
`Failed to get migration ${id}: ${error.message}`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
237
|
-
var getSql = (text, direction) => {
|
|
238
|
-
const rx = {
|
|
239
|
-
up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
|
|
240
|
-
down: /---- DOWN ----\n([\s\S]*)$/
|
|
241
|
-
};
|
|
242
|
-
return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
|
|
243
|
-
};
|
|
244
|
-
var getNewMigrations = (applied, files) => {
|
|
245
|
-
const sortedFiles = files.sort();
|
|
246
|
-
for (let ix = 0; ix < applied.length; ix++) {
|
|
247
|
-
if (sortedFiles[ix] !== applied[ix].id) {
|
|
248
|
-
throw new Error(
|
|
249
|
-
`Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const newMigrations = sortedFiles.slice(applied.length);
|
|
254
|
-
return newMigrations;
|
|
255
|
-
};
|
|
256
|
-
var createMigration = async (path, description) => {
|
|
257
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
258
|
-
const compactTimestamp = timestamp.replace(/[-:.TZ]/g, "");
|
|
259
|
-
const parsedDescription = description.replace(/\s+/g, "_").toLowerCase();
|
|
260
|
-
const filename = `${compactTimestamp}_${parsedDescription}.sql`;
|
|
261
|
-
const filePath = (0, import_node_path.join)(path, filename);
|
|
262
|
-
const template = `-- Created: ${timestamp}
|
|
263
|
-
-- Description: ${description}
|
|
264
|
-
|
|
265
|
-
---- UP ----
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
---- DOWN ----
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
`;
|
|
273
|
-
try {
|
|
274
|
-
await (0, import_promises.mkdir)(path, { recursive: true });
|
|
275
|
-
await (0, import_promises.writeFile)(filePath, template.trim(), "utf8");
|
|
276
|
-
console.log(`Migration created: ${filePath}`);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
throw new Error(`Error creating migration: ${error.message}`);
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
var writeConfig = async (path, config) => {
|
|
282
|
-
try {
|
|
283
|
-
const configContent = JSON.stringify(config, null, 2);
|
|
284
|
-
await (0, import_promises.writeFile)(path, configContent, "utf8");
|
|
285
|
-
console.log(`Configuration written to ${path}`);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
throw new Error(
|
|
288
|
-
`Error writing configuration to ${path}: ${error.message}`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
// src/index.ts
|
|
294
|
-
var getDb = ({ projectId, databaseName, instanceName }) => {
|
|
295
|
-
const spanner = projectId ? new import_spanner.Spanner({ projectId }) : new import_spanner.Spanner();
|
|
296
|
-
return spanner.instance(instanceName).database(databaseName);
|
|
297
|
-
};
|
|
298
|
-
var init = async (config, configPath) => {
|
|
299
|
-
await writeConfig(configPath, config);
|
|
300
|
-
};
|
|
301
|
-
var create = async (config, description) => {
|
|
302
|
-
await createMigration(config.migrationsPath, description);
|
|
303
|
-
};
|
|
304
|
-
var up = async (config, database, max) => {
|
|
305
|
-
if (max && !database) {
|
|
306
|
-
throw new Error("Max number of migrations requires specifying a database");
|
|
307
|
-
}
|
|
308
|
-
const databases = database ? [database] : config.instance.databases;
|
|
309
|
-
for (const databaseConfig of databases) {
|
|
310
|
-
const path = {
|
|
311
|
-
projectId: config.projectId,
|
|
312
|
-
instanceName: config.instance.name,
|
|
313
|
-
databaseName: databaseConfig.name
|
|
314
|
-
};
|
|
315
|
-
const db = getDb(path);
|
|
316
|
-
await ensureMigrationTable(db);
|
|
317
|
-
const appliedMigrations = await getAppliedMigrations(db);
|
|
318
|
-
const migrationFiles = await getMigrationFiles(
|
|
319
|
-
databaseConfig.migrationsPath
|
|
320
|
-
);
|
|
321
|
-
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
322
|
-
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
323
|
-
console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
|
|
324
|
-
const newMigrationsToApply = max ? newMigrations.slice(0, max) : newMigrations;
|
|
325
|
-
for (const id of newMigrationsToApply) {
|
|
326
|
-
const migration = await getMigration(databaseConfig.migrationsPath, id);
|
|
327
|
-
await applyUp(db, migration);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
var down = async (config, database) => {
|
|
332
|
-
const path = {
|
|
333
|
-
projectId: config.projectId,
|
|
334
|
-
instanceName: config.instance.name,
|
|
335
|
-
databaseName: database.name
|
|
336
|
-
};
|
|
337
|
-
const db = getDb(path);
|
|
338
|
-
await ensureMigrationTable(db);
|
|
339
|
-
await applyDown(db);
|
|
340
|
-
};
|
|
341
|
-
var status = async (config, databases) => {
|
|
342
|
-
const statuses = [];
|
|
343
|
-
for (const databaseConfig of databases || config.instance.databases) {
|
|
344
|
-
const path = {
|
|
345
|
-
projectId: config.projectId,
|
|
346
|
-
instanceName: config.instance.name,
|
|
347
|
-
databaseName: databaseConfig.name
|
|
348
|
-
};
|
|
349
|
-
const db = getDb(path);
|
|
350
|
-
await ensureMigrationTable(db);
|
|
351
|
-
const appliedMigrations = await getAppliedMigrations(db);
|
|
352
|
-
const migrationFiles = await getMigrationFiles(
|
|
353
|
-
databaseConfig.migrationsPath
|
|
354
|
-
);
|
|
355
|
-
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
356
|
-
statuses.push(
|
|
357
|
-
[
|
|
358
|
-
`Migrations [${databaseConfig.name}]`,
|
|
359
|
-
"",
|
|
360
|
-
"Applied",
|
|
361
|
-
"--------------------------------------------------------------------------------",
|
|
362
|
-
`${appliedMigrations.map((m) => m.id).join("\n")}
|
|
363
|
-
`,
|
|
364
|
-
"New",
|
|
365
|
-
"--------------------------------------------------------------------------------",
|
|
366
|
-
`${newMigrations.join("\n")}
|
|
367
|
-
`
|
|
368
|
-
].join("\n")
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
return statuses.join("\n\n");
|
|
372
|
-
};
|
|
373
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
374
|
-
0 && (module.exports = {
|
|
1
|
+
import {
|
|
375
2
|
create,
|
|
376
3
|
down,
|
|
377
4
|
init,
|
|
378
5
|
status,
|
|
379
6
|
up
|
|
380
|
-
}
|
|
7
|
+
} from "./chunk-XP6DPC5R.js";
|
|
8
|
+
export {
|
|
9
|
+
create,
|
|
10
|
+
down,
|
|
11
|
+
init,
|
|
12
|
+
status,
|
|
13
|
+
up
|
|
14
|
+
};
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebspark/spanner-migrate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
8
9
|
"files": [
|
|
9
10
|
"dist"
|
|
10
11
|
],
|
|
11
12
|
"bin": "./dist/cli.js",
|
|
12
13
|
"scripts": {
|
|
13
|
-
"build": "tsup-node src/index.ts src/cli.ts --format esm
|
|
14
|
+
"build": "tsup-node src/index.ts src/cli.ts --format esm --target node22 --dts",
|
|
14
15
|
"dev": "tsc --watch --noEmit",
|
|
15
16
|
"lint": "biome check .",
|
|
16
17
|
"test": "vitest run --passWithNoTests --coverage",
|
|
@@ -20,9 +21,9 @@
|
|
|
20
21
|
"devDependencies": {
|
|
21
22
|
"@google-cloud/spanner": "8.2.2",
|
|
22
23
|
"@sebspark/cli-tester": "*",
|
|
24
|
+
"@sebspark/tsconfig": "*",
|
|
23
25
|
"@types/yargs": "17.0.34",
|
|
24
|
-
"testcontainers": "11.7.2"
|
|
25
|
-
"tsconfig": "*"
|
|
26
|
+
"testcontainers": "11.7.2"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
28
29
|
"@google-cloud/spanner": "*"
|
package/dist/cli.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
package/dist/cli.mjs
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
create,
|
|
4
|
-
down,
|
|
5
|
-
init,
|
|
6
|
-
status,
|
|
7
|
-
up
|
|
8
|
-
} from "./chunk-YL5IK6VK.mjs";
|
|
9
|
-
|
|
10
|
-
// src/cli.ts
|
|
11
|
-
import fs from "fs/promises";
|
|
12
|
-
import { join } from "path";
|
|
13
|
-
import { input, select } from "@inquirer/prompts";
|
|
14
|
-
import yargs from "yargs";
|
|
15
|
-
import { hideBin } from "yargs/helpers";
|
|
16
|
-
var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
17
|
-
yargs(hideBin(process.argv)).scriptName("spanner-migrate").usage("$0 <command>").command(
|
|
18
|
-
"init",
|
|
19
|
-
"Initialize a .spanner-migrate.config.json file",
|
|
20
|
-
async () => {
|
|
21
|
-
const instanceName = await input({
|
|
22
|
-
message: "Enter Spanner instance name",
|
|
23
|
-
required: true
|
|
24
|
-
});
|
|
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
|
-
}
|
|
33
|
-
const projectId = await input({
|
|
34
|
-
message: "Enter Google Cloud project name",
|
|
35
|
-
required: false
|
|
36
|
-
});
|
|
37
|
-
const config = {
|
|
38
|
-
instance: {
|
|
39
|
-
name: instanceName,
|
|
40
|
-
databases
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
if (projectId) config.projectId = projectId;
|
|
44
|
-
await init(config, CONFIG_FILE);
|
|
45
|
-
}
|
|
46
|
-
).command(
|
|
47
|
-
"create <description ...>",
|
|
48
|
-
"Create a new migration file",
|
|
49
|
-
(yargs2) => {
|
|
50
|
-
yargs2.option("database", {
|
|
51
|
-
alias: "d",
|
|
52
|
-
type: "string",
|
|
53
|
-
describe: "Database name"
|
|
54
|
-
}).positional("description", {
|
|
55
|
-
type: "string",
|
|
56
|
-
describe: "Description of the migration",
|
|
57
|
-
demandOption: true
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
async (args) => {
|
|
61
|
-
const config = await loadConfig();
|
|
62
|
-
const fullDescription = args.description.join(" ");
|
|
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);
|
|
86
|
-
console.log(
|
|
87
|
-
`Migration file created: '${join(databaseConfig.migrationsPath, args.description.join("_"))}.sql'`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
).command(
|
|
91
|
-
"up",
|
|
92
|
-
"Apply migrations",
|
|
93
|
-
(yargs2) => {
|
|
94
|
-
yargs2.option("database", {
|
|
95
|
-
alias: "d",
|
|
96
|
-
type: "string",
|
|
97
|
-
describe: "Database name",
|
|
98
|
-
requiresArg: false
|
|
99
|
-
}).option("max", {
|
|
100
|
-
alias: "m",
|
|
101
|
-
type: "number",
|
|
102
|
-
describe: "Maximum number of migrations to apply (requires --database)",
|
|
103
|
-
requiresArg: false
|
|
104
|
-
});
|
|
105
|
-
},
|
|
106
|
-
async (args) => {
|
|
107
|
-
const config = await loadConfig();
|
|
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
|
-
}
|
|
131
|
-
console.log("Migrations applied successfully.");
|
|
132
|
-
}
|
|
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
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
type DatabaseConfig = {
|
|
2
|
-
name: string;
|
|
3
|
-
migrationsPath: string;
|
|
4
|
-
};
|
|
5
|
-
type InstanceConfig = {
|
|
6
|
-
name: string;
|
|
7
|
-
databases: DatabaseConfig[];
|
|
8
|
-
};
|
|
9
|
-
type Config = {
|
|
10
|
-
instance: InstanceConfig;
|
|
11
|
-
projectId?: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
declare const init: (config: Config, configPath: string) => Promise<void>;
|
|
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>;
|
|
19
|
-
|
|
20
|
-
export { create, down, init, status, up };
|
package/dist/index.mjs
DELETED
|
File without changes
|