@getxflow/cli 1.8.0 → 1.9.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/args.js +1 -0
- package/dist/bin.js +9 -1
- package/dist/commands/db.js +87 -6
- package/dist/flags.js +6 -3
- package/dist/help.js +672 -647
- package/dist/limits.js +3 -4
- package/dist/moment.js +24 -0
- package/dist/version.js +1 -1
- package/package.json +24 -24
- package/skills/xflow/SKILL.md +700 -685
package/dist/args.js
CHANGED
package/dist/bin.js
CHANGED
|
@@ -183,11 +183,19 @@ async function run(args) {
|
|
|
183
183
|
await (0, logs_1.dbLogs)(args);
|
|
184
184
|
return;
|
|
185
185
|
}
|
|
186
|
+
if (second === 'branch' || second === 'branches') {
|
|
187
|
+
await (0, db_1.dbBranch)({ ...args, words: args.words.slice(2) });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (second === 'restore') {
|
|
191
|
+
await (0, db_1.dbRestore)(args);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
186
194
|
if (second === undefined || second === 'status') {
|
|
187
195
|
await (0, db_1.dbStatus)({ ...args, words: args.words.slice(2) });
|
|
188
196
|
return;
|
|
189
197
|
}
|
|
190
|
-
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, exec and
|
|
198
|
+
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, list, schema, query, exec, migrate, branch and restore');
|
|
191
199
|
case 'storage':
|
|
192
200
|
if (second === 'push') {
|
|
193
201
|
await (0, storage_1.storagePush)(rest);
|
package/dist/commands/db.js
CHANGED
|
@@ -7,6 +7,8 @@ exports.dbQuery = dbQuery;
|
|
|
7
7
|
exports.dbExec = dbExec;
|
|
8
8
|
exports.dbMigrate = dbMigrate;
|
|
9
9
|
exports.migrateBeforeDeploy = migrateBeforeDeploy;
|
|
10
|
+
exports.dbBranch = dbBranch;
|
|
11
|
+
exports.dbRestore = dbRestore;
|
|
10
12
|
const node_fs_1 = require("node:fs");
|
|
11
13
|
const node_path_1 = require("node:path");
|
|
12
14
|
const api_1 = require("../api");
|
|
@@ -14,6 +16,7 @@ const args_1 = require("../args");
|
|
|
14
16
|
const config_1 = require("../config");
|
|
15
17
|
const errors_1 = require("../errors");
|
|
16
18
|
const session_1 = require("../session");
|
|
19
|
+
const moment_1 = require("../moment");
|
|
17
20
|
const text_file_1 = require("../text-file");
|
|
18
21
|
const ui_1 = require("../ui");
|
|
19
22
|
const sources_1 = require("./sources");
|
|
@@ -171,7 +174,7 @@ async function dbQuery(args) {
|
|
|
171
174
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
172
175
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/query`, {
|
|
173
176
|
method: 'POST',
|
|
174
|
-
body: { sql },
|
|
177
|
+
body: { sql, branch: (0, args_1.flagString)(args, 'branch') },
|
|
175
178
|
timeoutMs: 60_000,
|
|
176
179
|
});
|
|
177
180
|
// The whole values, machine-readable, and the only thing printed: --json silences
|
|
@@ -217,7 +220,7 @@ async function dbExec(args) {
|
|
|
217
220
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
218
221
|
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/exec`, {
|
|
219
222
|
method: 'POST',
|
|
220
|
-
body: { sql, allow_destructive: (0, args_1.flagBool)(args, 'allow-destructive') },
|
|
223
|
+
body: { sql, allow_destructive: (0, args_1.flagBool)(args, 'allow-destructive'), branch: (0, args_1.flagString)(args, 'branch') },
|
|
221
224
|
timeoutMs: 120_000,
|
|
222
225
|
});
|
|
223
226
|
if ((0, args_1.flagBool)(args, 'json')) {
|
|
@@ -251,11 +254,13 @@ async function dbMigrate(args) {
|
|
|
251
254
|
const client = await (0, session_1.connectProject)(config.projectId, root, config);
|
|
252
255
|
const migrations = requireMigrations(root);
|
|
253
256
|
const dryRun = (0, args_1.flagBool)(args, 'dry-run');
|
|
257
|
+
const branch = (0, args_1.flagString)(args, 'branch');
|
|
254
258
|
// Migrations live in the revisions of the project root: without this a project
|
|
255
259
|
// without functions would lose them from its history, and a clone would miss them.
|
|
256
|
-
|
|
260
|
+
// A try on a branch is not history yet.
|
|
261
|
+
if (!dryRun && !branch)
|
|
257
262
|
await (0, sources_1.pushSources)({ kind: 'project', root, projectId: config.projectId }, client, { force: false });
|
|
258
|
-
await migrate(client, config.projectId, migrations, dryRun, (0, args_1.flagBool)(args, 'allow-destructive'));
|
|
263
|
+
await migrate(client, config.projectId, migrations, dryRun, (0, args_1.flagBool)(args, 'allow-destructive'), branch);
|
|
259
264
|
}
|
|
260
265
|
/** The functions deploy runs this first: new code must not meet an old schema. */
|
|
261
266
|
async function migrateBeforeDeploy(client, root, projectId) {
|
|
@@ -271,14 +276,16 @@ async function migrateBeforeDeploy(client, root, projectId) {
|
|
|
271
276
|
throw e;
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
|
-
async function migrate(client, projectId, migrations, dryRun, allowDestructive) {
|
|
275
|
-
|
|
279
|
+
async function migrate(client, projectId, migrations, dryRun, allowDestructive, branch) {
|
|
280
|
+
const where = branch ? ` to branch ${branch}` : '';
|
|
281
|
+
(0, ui_1.step)(dryRun ? `Checking the migrations${where} (${migrations.length} in the directory)` : `Applying the migrations${where} (${migrations.length} in the directory)`);
|
|
276
282
|
const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/migrate`, {
|
|
277
283
|
method: 'POST',
|
|
278
284
|
body: {
|
|
279
285
|
migrations,
|
|
280
286
|
dry_run: dryRun,
|
|
281
287
|
allow_destructive: allowDestructive,
|
|
288
|
+
branch,
|
|
282
289
|
},
|
|
283
290
|
timeoutMs: 300_000,
|
|
284
291
|
});
|
|
@@ -314,3 +321,77 @@ async function migrate(client, projectId, migrations, dryRun, allowDestructive)
|
|
|
314
321
|
: 'The database is unchanged: the migration rolled back as a whole');
|
|
315
322
|
}
|
|
316
323
|
}
|
|
324
|
+
/** Branches of the project database: list, create, delete. */
|
|
325
|
+
async function dbBranch(args) {
|
|
326
|
+
const [sub, name] = args.words;
|
|
327
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
328
|
+
const path = `/api/v1/projects/${projectId}/db/branches`;
|
|
329
|
+
if (sub === undefined || sub === 'list') {
|
|
330
|
+
const data = await (0, api_1.apiJson)(client, path);
|
|
331
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
332
|
+
(0, ui_1.out)(JSON.stringify(data.branches, null, 2));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (data.branches.length === 0) {
|
|
336
|
+
(0, ui_1.note)('No branches');
|
|
337
|
+
(0, ui_1.note)((0, ui_1.dim)(' A copy to try things on: xflow db branch create <name> [--at 2h]'));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
(0, ui_1.table)(data.branches.map((b) => [b.name, b.at ? `as of ${new Date(b.at).toLocaleString()}` : 'as made', (0, ui_1.formatAge)(b.created_at)]));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!name)
|
|
344
|
+
throw new errors_1.CliError(`xflow db branch ${sub} needs a name`, `For example: xflow db branch ${sub} fix-orders`);
|
|
345
|
+
if (sub === 'create') {
|
|
346
|
+
const at = (0, args_1.flagString)(args, 'at');
|
|
347
|
+
const data = await (0, api_1.apiJson)(client, path, {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
body: { name, at: at ? (0, moment_1.parseMoment)(at) : undefined },
|
|
350
|
+
timeoutMs: 180_000,
|
|
351
|
+
});
|
|
352
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
353
|
+
(0, ui_1.out)(JSON.stringify(data.branch, null, 2));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
(0, ui_1.ok)(`Branch ${name}: the project database ${data.branch.at ? `as of ${new Date(data.branch.at).toLocaleString()}` : 'as it is now'}`);
|
|
357
|
+
(0, ui_1.note)((0, ui_1.dim)(` Work in it: xflow db query --branch ${name} '...', db exec and db migrate take --branch too`));
|
|
358
|
+
(0, ui_1.note)((0, ui_1.dim)(` Rows back into the project database: xflow db restore --from ${name} --table <table>`));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (sub === 'delete' || sub === 'rm') {
|
|
362
|
+
await (0, api_1.apiJson)(client, `${path}/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
363
|
+
(0, ui_1.ok)(`Branch ${name} deleted; the project database is unchanged`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
throw new errors_1.CliError(`Unknown command: db branch ${sub}`, 'Available: list, create and delete');
|
|
367
|
+
}
|
|
368
|
+
/** Rows of a table from a branch back into the project database, by primary key. */
|
|
369
|
+
async function dbRestore(args) {
|
|
370
|
+
const from = (0, args_1.flagString)(args, 'from');
|
|
371
|
+
const tableName = (0, args_1.flagString)(args, 'table');
|
|
372
|
+
if (!from || !tableName) {
|
|
373
|
+
throw new errors_1.CliError('Name the branch and the table', "For example: xflow db restore --from before-import --table orders --where 'id in (7, 9)'");
|
|
374
|
+
}
|
|
375
|
+
const overwrite = (0, args_1.flagBool)(args, 'overwrite');
|
|
376
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
377
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/restore`, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
body: { branch: from, table: tableName, where: (0, args_1.flagString)(args, 'where'), overwrite },
|
|
380
|
+
timeoutMs: 120_000,
|
|
381
|
+
});
|
|
382
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
383
|
+
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (data.read === 0) {
|
|
387
|
+
(0, ui_1.note)(`No rows of ${tableName} in branch ${from} match`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
(0, ui_1.ok)(`${data.restored} of ${data.read} rows of ${tableName} from branch ${from} ${overwrite ? 'written' : 'put back'}`);
|
|
391
|
+
for (const dump of data.dumps ?? []) {
|
|
392
|
+
(0, ui_1.note)((0, ui_1.dim)(` Copy of ${dump.table} before the change: ${dump.rows} rows, kept for ${data.dump_retention_days} days`));
|
|
393
|
+
}
|
|
394
|
+
if (!overwrite && data.restored < data.read) {
|
|
395
|
+
(0, ui_1.note)((0, ui_1.dim)(` The other ${data.read - data.restored} are in the database already and kept as they are: --overwrite replaces them`));
|
|
396
|
+
}
|
|
397
|
+
}
|
package/dist/flags.js
CHANGED
|
@@ -78,10 +78,13 @@ exports.KNOWN = {
|
|
|
78
78
|
'db status': ['project'],
|
|
79
79
|
'db list': [],
|
|
80
80
|
'db schema': ['project'],
|
|
81
|
-
'db query': ['json', 'project'],
|
|
82
|
-
'db exec': ['allow-destructive', 'json', 'project'],
|
|
81
|
+
'db query': ['json', 'project', 'branch'],
|
|
82
|
+
'db exec': ['allow-destructive', 'json', 'project', 'branch'],
|
|
83
83
|
'db logs': ['limit', 'project', 'cursor'],
|
|
84
|
-
'db migrate': ['dry-run', 'allow-destructive'],
|
|
84
|
+
'db migrate': ['dry-run', 'allow-destructive', 'branch'],
|
|
85
|
+
'db branch': ['at', 'json', 'project'],
|
|
86
|
+
'db branches': ['at', 'json', 'project'],
|
|
87
|
+
'db restore': ['from', 'table', 'where', 'overwrite', 'json', 'project'],
|
|
85
88
|
storage: ['json', 'project'],
|
|
86
89
|
'storage ls': ['json', 'project'],
|
|
87
90
|
'storage list': ['json', 'project'],
|