@sebspark/spanner-migrate 0.2.0 → 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/README.md +145 -59
- package/dist/{chunk-Z3ZLNDVO.mjs → chunk-SZDH364K.mjs} +67 -41
- package/dist/cli.js +228 -80
- package/dist/cli.mjs +160 -38
- package/dist/index.d.mts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +67 -41
- package/dist/index.mjs +1 -1
- package/package.json +14 -9
package/README.md
CHANGED
|
@@ -14,110 +14,196 @@ yarn add -D @sebspark/spanner-migrate
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
-
## CLI
|
|
17
|
+
## CLI Commands
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
`spanner-migrate` provides several commands for managing database migrations in Google Spanner.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
spanner-migrate [command] [options]
|
|
23
|
-
```
|
|
21
|
+
### Initialize Configuration
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
```sh
|
|
24
|
+
spanner-migrate init
|
|
25
|
+
```
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
Initializes a `.spanner-migrate.config.json` file by prompting for:
|
|
28
|
+
- Spanner instance name
|
|
29
|
+
- One or more database configurations
|
|
30
|
+
- Optional Google Cloud project name
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
### Create a Migration
|
|
31
33
|
|
|
32
|
-
```
|
|
33
|
-
spanner-migrate
|
|
34
|
+
```sh
|
|
35
|
+
spanner-migrate create <description ...> [--database <name>]
|
|
36
|
+
spanner-migrate create add users table
|
|
37
|
+
spanner-migrate create --database=mydb add users table
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
- `Enter the path for your migrations`: Directory for migration files (default: `./migrations`).
|
|
38
|
-
- `Enter Spanner instance name`: The name of the Spanner instance.
|
|
39
|
-
- `Enter Spanner database name`: The name of the Spanner database.
|
|
40
|
-
- `Enter Google Cloud project name`: (Optional) The Google Cloud project name.
|
|
40
|
+
Creates a new migration file with the specified description.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
- If `--database` (`-d`) is provided, it uses the specified database.
|
|
43
|
+
- If multiple databases exist and none is specified, the user is prompted to select one.
|
|
44
|
+
- The filename is generated from the description (`<timestamp>_add_users_table.sql`).
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
Create a new migration file.
|
|
46
|
+
### Apply Migrations
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
```sh
|
|
49
|
+
spanner-migrate up
|
|
50
|
+
spanner-migrate up --database <name>
|
|
51
|
+
spanner-migrate up --database <name> --max <n>
|
|
52
|
+
```
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
Applies pending migrations.
|
|
55
|
+
|
|
56
|
+
- If **no** `--database` and `--max` are provided, applies all migrations to all databases.
|
|
57
|
+
- If `--database` (`-d`) is provided, applies migrations only to that database.
|
|
58
|
+
- If `--max` (`-m`) is provided, limits the number of migrations applied (requires `--database`).
|
|
59
|
+
- `--max` must be an integer greater than 0.
|
|
60
|
+
|
|
61
|
+
### Roll Back Last Migration
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
spanner-migrate down
|
|
65
|
+
spanner-migrate down --database <name>
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
Rolls back the last applied migration.
|
|
69
|
+
|
|
70
|
+
- If a **single** database exists, it is automatically selected.
|
|
71
|
+
- If multiple databases exist, `--database` is **required**.
|
|
72
|
+
- The specified `--database` must exist.
|
|
73
|
+
|
|
74
|
+
### Show Migration Status
|
|
54
75
|
|
|
55
|
-
|
|
76
|
+
```sh
|
|
77
|
+
spanner-migrate status
|
|
78
|
+
spanner-migrate status --database <name>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Displays migration status.
|
|
56
82
|
|
|
57
|
-
|
|
83
|
+
- If `--database` is specified, shows status for that database.
|
|
84
|
+
- If no `--database` is provided, shows status for all configured databases.
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
-- Created: 2025-01-20T14:56:38.000Z
|
|
61
|
-
-- Description: create table users
|
|
86
|
+
### Help
|
|
62
87
|
|
|
63
|
-
|
|
88
|
+
```sh
|
|
89
|
+
spanner-migrate --help
|
|
90
|
+
spanner-migrate <command> --help
|
|
91
|
+
```
|
|
64
92
|
|
|
93
|
+
Displays help for the CLI or a specific command.
|
|
65
94
|
|
|
95
|
+
---
|
|
66
96
|
|
|
67
|
-
|
|
97
|
+
## Programmatic Usage
|
|
68
98
|
|
|
99
|
+
In addition to the CLI, `spanner-migrate` can be used as a Node.js module to manage migrations programmatically.
|
|
69
100
|
|
|
101
|
+
### Importing
|
|
70
102
|
|
|
103
|
+
```typescript
|
|
104
|
+
import { init, create, up, down, status } from '@sebspark/spanner-migrate'
|
|
71
105
|
```
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
Apply pending migrations
|
|
107
|
+
### Initializing Configuration
|
|
75
108
|
|
|
76
|
-
|
|
109
|
+
```typescript
|
|
110
|
+
import { init, type Config } from '@sebspark/spanner-migrate'
|
|
77
111
|
|
|
78
|
-
|
|
79
|
-
|
|
112
|
+
const config: Config = {
|
|
113
|
+
instance: {
|
|
114
|
+
name: 'my-instance',
|
|
115
|
+
databases: [
|
|
116
|
+
{ name: 'mydb', migrationsPath: './migrations' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
projectId: 'my-gcp-project',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await init(config, '.spanner-migrate.config.json')
|
|
80
123
|
```
|
|
81
124
|
|
|
82
|
-
|
|
125
|
+
Writes the given configuration to a `.spanner-migrate.config.json` file.
|
|
83
126
|
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
### Creating a Migration
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { create, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
131
|
+
|
|
132
|
+
const databaseConfig: DatabaseConfig = {
|
|
133
|
+
name: 'mydb',
|
|
134
|
+
migrationsPath: './migrations',
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await create(databaseConfig, 'add users table')
|
|
86
138
|
```
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
Rollback one migration
|
|
140
|
+
Creates a new migration file for the specified database.
|
|
90
141
|
|
|
91
|
-
|
|
142
|
+
### Applying Migrations
|
|
92
143
|
|
|
93
|
-
```
|
|
94
|
-
spanner-migrate
|
|
144
|
+
```typescript
|
|
145
|
+
import { up, type Config, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
146
|
+
|
|
147
|
+
// Load configuration
|
|
148
|
+
const config: Config = /* Load from file or define inline */
|
|
149
|
+
|
|
150
|
+
// Apply all migrations to all databases
|
|
151
|
+
await up(config)
|
|
152
|
+
|
|
153
|
+
// Apply all migrations to a specific database
|
|
154
|
+
const databaseConfig: DatabaseConfig = config.instance.databases[0]
|
|
155
|
+
await up(config, databaseConfig)
|
|
156
|
+
|
|
157
|
+
// Apply up to 5 migrations to a specific database
|
|
158
|
+
await up(config, databaseConfig, 5)
|
|
95
159
|
```
|
|
96
160
|
|
|
97
|
-
|
|
98
|
-
|
|
161
|
+
- Applies pending migrations.
|
|
162
|
+
- If a database is specified, only applies migrations to that database.
|
|
163
|
+
- If `max` is specified, applies at most `max` migrations.
|
|
99
164
|
|
|
100
|
-
|
|
165
|
+
### Rolling Back Migrations
|
|
101
166
|
|
|
102
|
-
```
|
|
103
|
-
spanner-migrate
|
|
167
|
+
```typescript
|
|
168
|
+
import { up, type Config, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
169
|
+
|
|
170
|
+
const config: Config = /* Load from file */
|
|
171
|
+
const databaseConfig: DatabaseConfig = config.instance.databases[0]
|
|
172
|
+
|
|
173
|
+
// Roll back the last applied migration
|
|
174
|
+
await down(config, databaseConfig)
|
|
104
175
|
```
|
|
105
|
-
Displays an overview of applied and peding migrations
|
|
106
176
|
|
|
107
|
-
|
|
108
|
-
|
|
177
|
+
- Rolls back the last applied migration for the specified database.
|
|
178
|
+
- Requires a database to be specified.
|
|
179
|
+
|
|
180
|
+
### Checking Migration Status
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { up, type Config, type DatabaseConfig } from '@sebspark/spanner-migrate'
|
|
109
184
|
|
|
110
|
-
|
|
111
|
-
--------------------------------------------------------------------------------
|
|
112
|
-
20250122080434866_add_users_table
|
|
113
|
-
20250122080444982_add_index_on_users
|
|
185
|
+
const config: Config = /* Load from file */
|
|
114
186
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
187
|
+
// Check status for all databases
|
|
188
|
+
const migrationStatus = await status(config)
|
|
189
|
+
console.log(migrationStatus)
|
|
190
|
+
|
|
191
|
+
// Check status for a specific database
|
|
192
|
+
const databaseConfig = config.instance.databases[0]
|
|
193
|
+
const migrationStatusSingle = await status(config, [databaseConfig])
|
|
194
|
+
console.log(migrationStatusSingle)
|
|
118
195
|
```
|
|
119
196
|
|
|
120
|
-
|
|
197
|
+
- Displays applied and pending migrations for one or more databases.
|
|
198
|
+
- If a specific database is provided, only its status is shown.
|
|
199
|
+
|
|
200
|
+
## Running on Spanner Emulator
|
|
201
|
+
|
|
202
|
+
If you want to test your migrations against a Spanner Emulator, you will need to set:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
process.env.SPANNER_EMULATOR_HOST = 'localhost:<port>'
|
|
206
|
+
```
|
|
121
207
|
|
|
122
208
|
## License
|
|
123
209
|
|
|
@@ -38,7 +38,7 @@ var applyDown = async (db) => {
|
|
|
38
38
|
json: true
|
|
39
39
|
};
|
|
40
40
|
const [rows] = await db.run(req);
|
|
41
|
-
const lastMigration = rows
|
|
41
|
+
const lastMigration = rows?.[0];
|
|
42
42
|
if (!lastMigration) {
|
|
43
43
|
throw new Error("No migrations found to roll back.");
|
|
44
44
|
}
|
|
@@ -100,12 +100,12 @@ var SQL_CREATE_TABLE_MIGRATIONS = `
|
|
|
100
100
|
down STRING(1024)
|
|
101
101
|
) PRIMARY KEY (id)
|
|
102
102
|
`;
|
|
103
|
-
var ensureMigrationTable = async (
|
|
104
|
-
const [rows] = await
|
|
103
|
+
var ensureMigrationTable = async (db) => {
|
|
104
|
+
const [rows] = await db.run(SQL_SELECT_TABLE_MIGRATIONS);
|
|
105
105
|
if (rows.length) return;
|
|
106
106
|
console.log("Creating migration table");
|
|
107
107
|
try {
|
|
108
|
-
await
|
|
108
|
+
await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
|
|
109
109
|
} catch (err) {
|
|
110
110
|
console.error("Failed to create migrations table");
|
|
111
111
|
throw err;
|
|
@@ -168,22 +168,17 @@ var getMigration = async (path, id) => {
|
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
|
-
var getDescription = (text) =>
|
|
172
|
-
var _a, _b;
|
|
173
|
-
return ((_b = (_a = text == null ? void 0 : text.match(/^--\s*Description:\s*(.+)$/m)) == null ? void 0 : _a[1]) == null ? void 0 : _b.trim()) || "";
|
|
174
|
-
};
|
|
171
|
+
var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
175
172
|
var getSql = (text, direction) => {
|
|
176
|
-
var _a, _b;
|
|
177
173
|
const rx = {
|
|
178
174
|
up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
|
|
179
175
|
down: /---- DOWN ----\n([\s\S]*)$/
|
|
180
176
|
};
|
|
181
|
-
return
|
|
177
|
+
return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
|
|
182
178
|
};
|
|
183
179
|
var getNewMigrations = (applied, files) => {
|
|
184
180
|
const sortedFiles = files.sort();
|
|
185
181
|
for (let ix = 0; ix < applied.length; ix++) {
|
|
186
|
-
console.log(sortedFiles[ix], applied[ix].id);
|
|
187
182
|
if (sortedFiles[ix] !== applied[ix].id) {
|
|
188
183
|
throw new Error(
|
|
189
184
|
`Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
|
|
@@ -191,7 +186,6 @@ var getNewMigrations = (applied, files) => {
|
|
|
191
186
|
}
|
|
192
187
|
}
|
|
193
188
|
const newMigrations = sortedFiles.slice(applied.length);
|
|
194
|
-
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
195
189
|
return newMigrations;
|
|
196
190
|
};
|
|
197
191
|
var createMigration = async (path, description) => {
|
|
@@ -242,42 +236,74 @@ var init = async (config, configPath) => {
|
|
|
242
236
|
var create = async (config, description) => {
|
|
243
237
|
await createMigration(config.migrationsPath, description);
|
|
244
238
|
};
|
|
245
|
-
var up = async (config, max
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
239
|
+
var up = async (config, database, max) => {
|
|
240
|
+
if (max && !database) {
|
|
241
|
+
throw new Error("Max number of migrations requires specifying a database");
|
|
242
|
+
}
|
|
243
|
+
const databases = database ? [database] : config.instance.databases;
|
|
244
|
+
for (const databaseConfig of databases) {
|
|
245
|
+
const path = {
|
|
246
|
+
projectId: config.projectId,
|
|
247
|
+
instanceName: config.instance.name,
|
|
248
|
+
databaseName: databaseConfig.name
|
|
249
|
+
};
|
|
250
|
+
const db = getDb(path);
|
|
251
|
+
await ensureMigrationTable(db);
|
|
252
|
+
const appliedMigrations = await getAppliedMigrations(db);
|
|
253
|
+
const migrationFiles = await getMigrationFiles(
|
|
254
|
+
databaseConfig.migrationsPath
|
|
255
|
+
);
|
|
256
|
+
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
257
|
+
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
258
|
+
console.log(newMigrations.map((mig) => ` ${mig}`).join("\n"));
|
|
259
|
+
const newMigrationsToApply = max ? newMigrations.slice(0, max) : newMigrations;
|
|
260
|
+
for (const id of newMigrationsToApply) {
|
|
261
|
+
const migration = await getMigration(databaseConfig.migrationsPath, id);
|
|
262
|
+
await applyUp(db, migration);
|
|
263
|
+
}
|
|
256
264
|
}
|
|
257
265
|
};
|
|
258
|
-
var down = async (config) => {
|
|
259
|
-
const
|
|
266
|
+
var down = async (config, database) => {
|
|
267
|
+
const path = {
|
|
268
|
+
projectId: config.projectId,
|
|
269
|
+
instanceName: config.instance.name,
|
|
270
|
+
databaseName: database.name
|
|
271
|
+
};
|
|
272
|
+
const db = getDb(path);
|
|
260
273
|
await ensureMigrationTable(db);
|
|
261
274
|
await applyDown(db);
|
|
262
275
|
};
|
|
263
|
-
var status = async (config) => {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
276
|
+
var status = async (config, databases) => {
|
|
277
|
+
const statuses = [];
|
|
278
|
+
for (const databaseConfig of databases || config.instance.databases) {
|
|
279
|
+
const path = {
|
|
280
|
+
projectId: config.projectId,
|
|
281
|
+
instanceName: config.instance.name,
|
|
282
|
+
databaseName: databaseConfig.name
|
|
283
|
+
};
|
|
284
|
+
const db = getDb(path);
|
|
285
|
+
await ensureMigrationTable(db);
|
|
286
|
+
const appliedMigrations = await getAppliedMigrations(db);
|
|
287
|
+
const migrationFiles = await getMigrationFiles(
|
|
288
|
+
databaseConfig.migrationsPath
|
|
289
|
+
);
|
|
290
|
+
const newMigrations = getNewMigrations(appliedMigrations, migrationFiles);
|
|
291
|
+
statuses.push(
|
|
292
|
+
[
|
|
293
|
+
`Migrations [${databaseConfig.name}]`,
|
|
294
|
+
"",
|
|
295
|
+
"Applied",
|
|
296
|
+
"--------------------------------------------------------------------------------",
|
|
297
|
+
`${appliedMigrations.map((m) => m.id).join("\n")}
|
|
275
298
|
`,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
299
|
+
"New",
|
|
300
|
+
"--------------------------------------------------------------------------------",
|
|
301
|
+
`${newMigrations.join("\n")}
|
|
279
302
|
`
|
|
280
|
-
|
|
303
|
+
].join("\n")
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return statuses.join("\n\n");
|
|
281
307
|
};
|
|
282
308
|
|
|
283
309
|
export {
|
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
|
|
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
|
|
73
|
+
const lastMigration = rows?.[0];
|
|
74
74
|
if (!lastMigration) {
|
|
75
75
|
throw new Error("No migrations found to roll back.");
|
|
76
76
|
}
|
|
@@ -132,12 +132,12 @@ var SQL_CREATE_TABLE_MIGRATIONS = `
|
|
|
132
132
|
down STRING(1024)
|
|
133
133
|
) PRIMARY KEY (id)
|
|
134
134
|
`;
|
|
135
|
-
var ensureMigrationTable = async (
|
|
136
|
-
const [rows] = await
|
|
135
|
+
var ensureMigrationTable = async (db) => {
|
|
136
|
+
const [rows] = await db.run(SQL_SELECT_TABLE_MIGRATIONS);
|
|
137
137
|
if (rows.length) return;
|
|
138
138
|
console.log("Creating migration table");
|
|
139
139
|
try {
|
|
140
|
-
await
|
|
140
|
+
await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
|
|
141
141
|
} catch (err) {
|
|
142
142
|
console.error("Failed to create migrations table");
|
|
143
143
|
throw err;
|
|
@@ -200,22 +200,17 @@ var getMigration = async (path, id) => {
|
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
|
-
var getDescription = (text) =>
|
|
204
|
-
var _a, _b;
|
|
205
|
-
return ((_b = (_a = text == null ? void 0 : text.match(/^--\s*Description:\s*(.+)$/m)) == null ? void 0 : _a[1]) == null ? void 0 : _b.trim()) || "";
|
|
206
|
-
};
|
|
203
|
+
var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
207
204
|
var getSql = (text, direction) => {
|
|
208
|
-
var _a, _b;
|
|
209
205
|
const rx = {
|
|
210
206
|
up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
|
|
211
207
|
down: /---- DOWN ----\n([\s\S]*)$/
|
|
212
208
|
};
|
|
213
|
-
return
|
|
209
|
+
return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
|
|
214
210
|
};
|
|
215
211
|
var getNewMigrations = (applied, files) => {
|
|
216
212
|
const sortedFiles = files.sort();
|
|
217
213
|
for (let ix = 0; ix < applied.length; ix++) {
|
|
218
|
-
console.log(sortedFiles[ix], applied[ix].id);
|
|
219
214
|
if (sortedFiles[ix] !== applied[ix].id) {
|
|
220
215
|
throw new Error(
|
|
221
216
|
`Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
|
|
@@ -223,7 +218,6 @@ var getNewMigrations = (applied, files) => {
|
|
|
223
218
|
}
|
|
224
219
|
}
|
|
225
220
|
const newMigrations = sortedFiles.slice(applied.length);
|
|
226
|
-
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
227
221
|
return newMigrations;
|
|
228
222
|
};
|
|
229
223
|
var createMigration = async (path, description) => {
|
|
@@ -274,86 +268,116 @@ var init = async (config, configPath) => {
|
|
|
274
268
|
var create = async (config, description) => {
|
|
275
269
|
await createMigration(config.migrationsPath, description);
|
|
276
270
|
};
|
|
277
|
-
var up = async (config, max
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
+
}
|
|
288
296
|
}
|
|
289
297
|
};
|
|
290
|
-
var down = async (config) => {
|
|
291
|
-
const
|
|
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);
|
|
292
305
|
await ensureMigrationTable(db);
|
|
293
306
|
await applyDown(db);
|
|
294
307
|
};
|
|
295
|
-
var status = async (config) => {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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")}
|
|
307
330
|
`,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
331
|
+
"New",
|
|
332
|
+
"--------------------------------------------------------------------------------",
|
|
333
|
+
`${newMigrations.join("\n")}
|
|
311
334
|
`
|
|
312
|
-
|
|
335
|
+
].join("\n")
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return statuses.join("\n\n");
|
|
313
339
|
};
|
|
314
340
|
|
|
315
341
|
// src/cli.ts
|
|
316
342
|
var CONFIG_FILE = "./.spanner-migrate.config.json";
|
|
317
|
-
async function loadConfig() {
|
|
318
|
-
try {
|
|
319
|
-
const configContent = await import_promises2.default.readFile(CONFIG_FILE, "utf8");
|
|
320
|
-
return JSON.parse(configContent);
|
|
321
|
-
} catch {
|
|
322
|
-
console.error('Config file not found. Run "spanner-migrate init" first.');
|
|
323
|
-
process.exit(1);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
343
|
(0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).scriptName("spanner-migrate").usage("$0 <command>").command(
|
|
327
344
|
"init",
|
|
328
345
|
"Initialize a .spanner-migrate.config.json file",
|
|
329
346
|
async () => {
|
|
330
|
-
const
|
|
331
|
-
message: "Enter the path for your migrations",
|
|
332
|
-
required: true,
|
|
333
|
-
default: "./migrations"
|
|
334
|
-
});
|
|
335
|
-
const instanceName = await (0, import_input.default)({
|
|
347
|
+
const instanceName = await (0, import_prompts.input)({
|
|
336
348
|
message: "Enter Spanner instance name",
|
|
337
349
|
required: true
|
|
338
350
|
});
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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)({
|
|
344
360
|
message: "Enter Google Cloud project name",
|
|
345
361
|
required: false
|
|
346
362
|
});
|
|
347
|
-
const config = {
|
|
363
|
+
const config = {
|
|
364
|
+
instance: {
|
|
365
|
+
name: instanceName,
|
|
366
|
+
databases
|
|
367
|
+
}
|
|
368
|
+
};
|
|
348
369
|
if (projectId) config.projectId = projectId;
|
|
349
370
|
await init(config, CONFIG_FILE);
|
|
350
|
-
console.log(`Configuration written to ${CONFIG_FILE}`);
|
|
351
371
|
}
|
|
352
372
|
).command(
|
|
353
373
|
"create <description ...>",
|
|
354
374
|
"Create a new migration file",
|
|
355
375
|
(yargs2) => {
|
|
356
|
-
yargs2.
|
|
376
|
+
yargs2.option("database", {
|
|
377
|
+
alias: "d",
|
|
378
|
+
type: "string",
|
|
379
|
+
describe: "Database name"
|
|
380
|
+
}).positional("description", {
|
|
357
381
|
type: "string",
|
|
358
382
|
describe: "Description of the migration",
|
|
359
383
|
demandOption: true
|
|
@@ -362,33 +386,157 @@ async function loadConfig() {
|
|
|
362
386
|
async (args) => {
|
|
363
387
|
const config = await loadConfig();
|
|
364
388
|
const fullDescription = args.description.join(" ");
|
|
365
|
-
|
|
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);
|
|
366
412
|
console.log(
|
|
367
|
-
`Migration file created: '${(0, import_node_path2.join)(
|
|
413
|
+
`Migration file created: '${(0, import_node_path2.join)(databaseConfig.migrationsPath, args.description.join("_"))}.sql'`
|
|
368
414
|
);
|
|
369
415
|
}
|
|
370
416
|
).command(
|
|
371
417
|
"up",
|
|
372
418
|
"Apply migrations",
|
|
373
419
|
(yargs2) => {
|
|
374
|
-
yargs2.option("
|
|
420
|
+
yargs2.option("database", {
|
|
421
|
+
alias: "d",
|
|
422
|
+
type: "string",
|
|
423
|
+
describe: "Database name",
|
|
424
|
+
requiresArg: false
|
|
425
|
+
}).option("max", {
|
|
375
426
|
alias: "m",
|
|
376
427
|
type: "number",
|
|
377
|
-
describe: "Maximum number of migrations to apply",
|
|
378
|
-
|
|
428
|
+
describe: "Maximum number of migrations to apply (requires --database)",
|
|
429
|
+
requiresArg: false
|
|
379
430
|
});
|
|
380
431
|
},
|
|
381
432
|
async (args) => {
|
|
382
433
|
const config = await loadConfig();
|
|
383
|
-
|
|
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
|
+
}
|
|
384
457
|
console.log("Migrations applied successfully.");
|
|
385
458
|
}
|
|
386
|
-
).command(
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
})
|
|
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
|
+
};
|
package/dist/cli.mjs
CHANGED
|
@@ -5,55 +5,53 @@ import {
|
|
|
5
5
|
init,
|
|
6
6
|
status,
|
|
7
7
|
up
|
|
8
|
-
} from "./chunk-
|
|
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/
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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 = {
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
})
|
|
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
|
|
1
|
+
type DatabaseConfig = {
|
|
2
|
+
name: string;
|
|
2
3
|
migrationsPath: string;
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
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
|
|
1
|
+
type DatabaseConfig = {
|
|
2
|
+
name: string;
|
|
2
3
|
migrationsPath: string;
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
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
|
|
69
|
+
const lastMigration = rows?.[0];
|
|
70
70
|
if (!lastMigration) {
|
|
71
71
|
throw new Error("No migrations found to roll back.");
|
|
72
72
|
}
|
|
@@ -128,12 +128,12 @@ var SQL_CREATE_TABLE_MIGRATIONS = `
|
|
|
128
128
|
down STRING(1024)
|
|
129
129
|
) PRIMARY KEY (id)
|
|
130
130
|
`;
|
|
131
|
-
var ensureMigrationTable = async (
|
|
132
|
-
const [rows] = await
|
|
131
|
+
var ensureMigrationTable = async (db) => {
|
|
132
|
+
const [rows] = await db.run(SQL_SELECT_TABLE_MIGRATIONS);
|
|
133
133
|
if (rows.length) return;
|
|
134
134
|
console.log("Creating migration table");
|
|
135
135
|
try {
|
|
136
|
-
await
|
|
136
|
+
await db.updateSchema(SQL_CREATE_TABLE_MIGRATIONS);
|
|
137
137
|
} catch (err) {
|
|
138
138
|
console.error("Failed to create migrations table");
|
|
139
139
|
throw err;
|
|
@@ -196,22 +196,17 @@ var getMigration = async (path, id) => {
|
|
|
196
196
|
);
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
|
-
var getDescription = (text) =>
|
|
200
|
-
var _a, _b;
|
|
201
|
-
return ((_b = (_a = text == null ? void 0 : text.match(/^--\s*Description:\s*(.+)$/m)) == null ? void 0 : _a[1]) == null ? void 0 : _b.trim()) || "";
|
|
202
|
-
};
|
|
199
|
+
var getDescription = (text) => text?.match(/^--\s*Description:\s*(.+)$/m)?.[1]?.trim() || "";
|
|
203
200
|
var getSql = (text, direction) => {
|
|
204
|
-
var _a, _b;
|
|
205
201
|
const rx = {
|
|
206
202
|
up: /---- UP ----\n([\s\S]*?)\n---- DOWN ----/,
|
|
207
203
|
down: /---- DOWN ----\n([\s\S]*)$/
|
|
208
204
|
};
|
|
209
|
-
return
|
|
205
|
+
return text?.match(rx[direction])?.[1]?.replace(/--.*$/gm, "").trim();
|
|
210
206
|
};
|
|
211
207
|
var getNewMigrations = (applied, files) => {
|
|
212
208
|
const sortedFiles = files.sort();
|
|
213
209
|
for (let ix = 0; ix < applied.length; ix++) {
|
|
214
|
-
console.log(sortedFiles[ix], applied[ix].id);
|
|
215
210
|
if (sortedFiles[ix] !== applied[ix].id) {
|
|
216
211
|
throw new Error(
|
|
217
212
|
`Mismatch between applied migrations and files. Found '${sortedFiles[ix]}' but expected '${applied[ix].id}' at position ${ix}.`
|
|
@@ -219,7 +214,6 @@ var getNewMigrations = (applied, files) => {
|
|
|
219
214
|
}
|
|
220
215
|
}
|
|
221
216
|
const newMigrations = sortedFiles.slice(applied.length);
|
|
222
|
-
console.log(`Found ${newMigrations.length} new migrations.`);
|
|
223
217
|
return newMigrations;
|
|
224
218
|
};
|
|
225
219
|
var createMigration = async (path, description) => {
|
|
@@ -270,42 +264,74 @@ var init = async (config, configPath) => {
|
|
|
270
264
|
var create = async (config, description) => {
|
|
271
265
|
await createMigration(config.migrationsPath, description);
|
|
272
266
|
};
|
|
273
|
-
var up = async (config, max
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|
|
284
292
|
}
|
|
285
293
|
};
|
|
286
|
-
var down = async (config) => {
|
|
287
|
-
const
|
|
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);
|
|
288
301
|
await ensureMigrationTable(db);
|
|
289
302
|
await applyDown(db);
|
|
290
303
|
};
|
|
291
|
-
var status = async (config) => {
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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")}
|
|
303
326
|
`,
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
327
|
+
"New",
|
|
328
|
+
"--------------------------------------------------------------------------------",
|
|
329
|
+
`${newMigrations.join("\n")}
|
|
307
330
|
`
|
|
308
|
-
|
|
331
|
+
].join("\n")
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return statuses.join("\n\n");
|
|
309
335
|
};
|
|
310
336
|
// Annotate the CommonJS export names for ESM import in node:
|
|
311
337
|
0 && (module.exports = {
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sebspark/spanner-migrate",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
21
|
+
"@google-cloud/spanner": "7.18.1",
|
|
22
|
+
"@sebspark/cli-tester": "*",
|
|
22
23
|
"@sebspark/spanner-mock": "*",
|
|
23
|
-
"@types/jest": "
|
|
24
|
-
"@types/yargs": "
|
|
25
|
-
"jest": "
|
|
26
|
-
"testcontainers": "
|
|
27
|
-
"ts-jest": "
|
|
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/
|
|
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
|
}
|