@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 CHANGED
@@ -21,6 +21,7 @@ exports.BOOLEAN_FLAGS = new Set([
21
21
  'dry-run',
22
22
  'allow-destructive',
23
23
  'allow-removals',
24
+ 'overwrite',
24
25
  ]);
25
26
  function parseArgs(argv) {
26
27
  const words = [];
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 migrate');
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);
@@ -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
- if (!dryRun)
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
- (0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
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'],