@getxflow/cli 1.0.9 → 1.1.1

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.
@@ -6,6 +6,7 @@ exports.dbSchema = dbSchema;
6
6
  exports.dbQuery = dbQuery;
7
7
  exports.dbMove = dbMove;
8
8
  exports.dbMigrate = dbMigrate;
9
+ exports.migrateBeforeDeploy = migrateBeforeDeploy;
9
10
  const node_fs_1 = require("node:fs");
10
11
  const node_path_1 = require("node:path");
11
12
  const api_1 = require("../api");
@@ -244,7 +245,7 @@ async function dbQuery(args) {
244
245
  const { projectId, client } = await (0, session_1.projectTarget)(args);
245
246
  const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/query`, {
246
247
  method: 'POST',
247
- body: { sql, limit: (0, args_1.flagNumber)(args, 'limit') },
248
+ body: { sql },
248
249
  timeoutMs: 60_000,
249
250
  });
250
251
  // The whole values, machine-readable, and the only thing printed: --json silences
@@ -272,11 +273,10 @@ async function dbQuery(args) {
272
273
  }
273
274
  if (data.truncated) {
274
275
  // No exact total here: the platform stops reading at limit + 1 rows, so all
275
- // it knows is that more exist. The "raise --limit" advice shows only while
276
- // the flag still has room below the ceiling.
276
+ // it knows is that more exist.
277
277
  const room = data.limit !== undefined && data.limit_max !== undefined && data.limit < data.limit_max;
278
278
  (0, ui_1.note)((0, ui_1.dim)(room
279
- ? ` Shown the first ${data.rows.length} rows, more exist: raise --limit (up to ${data.limit_max}), or narrow the query`
279
+ ? ` Shown the first ${data.rows.length} rows, more exist: end the query with a larger limit (up to ${data.limit_max}), or narrow it`
280
280
  : ` Shown the first ${data.rows.length} rows, more exist: narrow the query, with a where clause or an offset`));
281
281
  }
282
282
  }
@@ -345,13 +345,30 @@ async function dbMigrate(args) {
345
345
  // without functions would lose them from its history, and a clone would miss them.
346
346
  if (!dryRun)
347
347
  await (0, sources_1.pushSources)({ kind: 'project', root, projectId: config.projectId }, client, { force: false });
348
+ await migrate(client, config.projectId, migrations, dryRun, (0, args_1.flagBool)(args, 'allow-destructive'));
349
+ }
350
+ /** The functions deploy runs this first: new code must not meet an old schema. */
351
+ async function migrateBeforeDeploy(client, root, projectId) {
352
+ const migrations = readMigrations(root);
353
+ if (migrations.length === 0)
354
+ return;
355
+ try {
356
+ await migrate(client, projectId, migrations, false, false);
357
+ }
358
+ catch (e) {
359
+ (0, ui_1.warn)('The functions were not deployed: the migrations stopped first');
360
+ (0, ui_1.note)((0, ui_1.dim)(' Once xflow db migrate goes through, deploy again'));
361
+ throw e;
362
+ }
363
+ }
364
+ async function migrate(client, projectId, migrations, dryRun, allowDestructive) {
348
365
  (0, ui_1.step)(dryRun ? `Checking the migrations (${migrations.length} in the directory)` : `Applying the migrations (${migrations.length} in the directory)`);
349
- const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/migrate`, {
366
+ const result = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/db/migrate`, {
350
367
  method: 'POST',
351
368
  body: {
352
369
  migrations,
353
370
  dry_run: dryRun,
354
- allow_destructive: (0, args_1.flagBool)(args, 'allow-destructive'),
371
+ allow_destructive: allowDestructive,
355
372
  },
356
373
  timeoutMs: 300_000,
357
374
  });
@@ -14,6 +14,7 @@ const prompt_1 = require("../prompt");
14
14
  const session_1 = require("../session");
15
15
  const tree_1 = require("../tree");
16
16
  const ui_1 = require("../ui");
17
+ const db_1 = require("./db");
17
18
  const sources_1 = require("./sources");
18
19
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
19
20
  /** Slightly longer than the platform's own build timeout. */
@@ -166,7 +167,7 @@ async function warnUndeployedRoot(client, projectRoot, projectId) {
166
167
  const relation = server ? (0, tree_1.syncState)(tree.hash, server.tree_hash, (0, config_1.readState)(projectRoot).treeHash) : 'local-changes';
167
168
  if (relation !== 'matches') {
168
169
  (0, ui_1.warn)('The project root holds changes nobody deployed (functions, migrations or callers)');
169
- (0, ui_1.note)((0, ui_1.dim)(' If this app needs them: xflow db migrate and xflow deploy functions first'));
170
+ (0, ui_1.note)((0, ui_1.dim)(' If this app needs them: xflow deploy functions first, it applies the migrations too'));
170
171
  }
171
172
  }
172
173
  catch {
@@ -199,6 +200,8 @@ async function deployTarget(target, args, comment, revision) {
199
200
  : { kind: 'project', root: target.root, projectId: target.projectId };
200
201
  revision = (await (0, sources_1.pushSources)(stream, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
201
202
  }
203
+ if (target.kind === 'project')
204
+ await (0, db_1.migrateBeforeDeploy)(client, target.root, target.projectId);
202
205
  }
203
206
  (0, ui_1.step)(target.kind === 'app' ? `Building the app from revision ${revision}` : `Deploying the functions from revision ${revision}`);
204
207
  const started = await startBuild(client, base, revision, (0, args_1.flagBool)(args, 'allow-removals'), comment);
@@ -24,7 +24,7 @@ async function functionsList(args) {
24
24
  fn.last_called_at ? `called ${(0, ui_1.formatAge)(fn.last_called_at)}` : 'never called',
25
25
  fn.last_error_at ? `failed ${(0, ui_1.formatAge)(fn.last_error_at)}` : '',
26
26
  ]));
27
- (0, ui_1.note)((0, ui_1.dim)(' callers: the app folders and page names allowed to call it (functions in the root xflow.json)'));
27
+ (0, ui_1.note)((0, ui_1.dim)(' callers: the app folders allowed to call it (functions in the root xflow.json)'));
28
28
  (0, ui_1.note)((0, ui_1.dim)(' open: a caller is public, so anyone on the internet can call it; no callers: a workflow'));
29
29
  (0, ui_1.note)((0, ui_1.dim)(` Webhooks and other functions call ${functions[0].url.replace(/\/[^/]+$/, '/<name>')} with a key`));
30
30
  }
@@ -99,7 +99,6 @@ async function pagesCreate(args) {
99
99
  (0, ui_1.ok)(`Page "${result.page.title}" is live: ${result.file_count} file(s), ${(0, ui_1.formatBytes)(result.size_bytes)}`);
100
100
  (0, ui_1.out)(result.page.url);
101
101
  (0, ui_1.note)((0, ui_1.dim)(` Anyone with the address sees it within seconds. The next version: xflow pages deploy ${result.page.slug} <path>`));
102
- (0, ui_1.note)((0, ui_1.dim)(` A backend for it: name "${result.page.slug}" in functions.<name>.callers of a project, xflow deploy functions, then fetch /_fn/<name>`));
103
102
  }
104
103
  function contentTypeOf(upload) {
105
104
  return upload.kind === 'zip' ? 'application/zip' : 'text/html';
package/dist/flags.js CHANGED
@@ -80,7 +80,7 @@ exports.KNOWN = {
80
80
  'db status': ['project'],
81
81
  'db list': [],
82
82
  'db schema': ['project'],
83
- 'db query': ['limit', 'json', 'project'],
83
+ 'db query': ['json', 'project'],
84
84
  'db migrate': ['dry-run', 'allow-destructive'],
85
85
  'db move': ['to', 'confirm', 'project'],
86
86
  storage: ['json', 'project'],
package/dist/help.js CHANGED
@@ -301,11 +301,10 @@ build it and upload what the build writes. Dot entries are skipped. At most 5 MB
301
301
  unpacked and 1000 files.
302
302
 
303
303
  The page is public, so a file holding a secret key is refused whole: every visitor would read
304
- it. A page that needs a backend (a form, a lookup) calls ${(0, ui_1.bold)('/_fn/<name>')} on its own address,
305
- and only a function whose callers in the root xflow.json of its project name the page answers:
306
- add the page name there and run ${(0, ui_1.bold)('xflow deploy functions')}. The page has to exist first.
307
- Such a function is open to the whole internet. Search engines are asked not to index pages,
308
- so do not promise SEO.
304
+ it. A page has no backend: a form or a lookup belongs in an app with access "anyone". The one
305
+ key a page may hold is a function key, and it opens that function to every visitor: an HTML
306
+ form can post to its webhook address with ${(0, ui_1.bold)('?key=')}, while fetch cannot read the answer
307
+ (no CORS). Search engines are asked not to index pages, so do not promise SEO.
309
308
 
310
309
  ${(0, ui_1.bold)('Deleting is final.')} The address goes empty and every version of the page is gone: there
311
310
  is no undo and no copy on the platform side. The name stays reserved for the organization, so
@@ -343,8 +342,8 @@ will work.`,
343
342
  xflow db status what is applied, what is waiting, the server
344
343
  xflow db list databases of the projects, the servers of the organization
345
344
  xflow db schema [table] tables of the project, or the columns of one
346
- xflow db query 'select ...' read data (--limit N: 50 by default, 300 at most;
347
- --json prints the values whole)
345
+ xflow db query 'select ...' read data (rows: the query's own limit, 50 without
346
+ one, 300 at most; --json prints the values whole)
348
347
  xflow db query <file>.sql the same, the query read from a file
349
348
  xflow db migrate store the project root, then apply migrations/*.sql
350
349
  xflow db move --to <server> --confirm <project name>
@@ -618,14 +617,14 @@ of the project runs the new code the moment it finishes; there is no publish ste
618
617
 
619
618
  Who may call a function is its callers, in the root xflow.json:
620
619
 
621
- { "projectId": "…", "functions": { "book-slot": { "callers": ["zapis", "clinic-landing"] } } }
620
+ { "projectId": "…", "functions": { "book-slot": { "callers": ["zapis"] } } }
622
621
 
623
- A caller is an app folder of this project (apps/zapis is "zapis") or the name of a page of
624
- the organization. A function nobody lists has no callers: a workflow, woken by a schedule,
625
- by a service holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a
626
- caller is public (a page, or an app anyone with the link opens), so anyone on the internet
627
- can call it. The address in the note under the table is for webhooks and other functions,
628
- with a key; apps and pages call ${(0, ui_1.bold)('/_fn/<name>')} on their own address.
622
+ A caller is an app folder of this project (apps/zapis is "zapis"); pages are static and call
623
+ nothing. A function nobody lists has no callers: a workflow, woken by a schedule, by a service
624
+ holding a key a person issued, or by ${(0, ui_1.bold)('invoke')}. ${(0, ui_1.bold)('open')} in the list means a caller is an app
625
+ anyone with the link opens, so anyone on the internet can call it. The address in the note
626
+ under the table is for webhooks and other functions, with a key; apps call ${(0, ui_1.bold)('/_fn/<name>')} on
627
+ their own address.
629
628
 
630
629
  ${(0, ui_1.bold)('invoke')} calls it as the person who owns the key: the platform checks the role, gives
631
630
  the function a one-minute ticket and calls it itself, so the function sees
@@ -718,8 +717,8 @@ git's job.
718
717
  --force overwrite the folder completely`,
719
718
  deploy: `${(0, ui_1.bold)('xflow deploy')}: deploy the functions or build an app, then publish
720
719
 
721
- xflow deploy functions -m "…" send the project root, check it, ship the functions
722
- and their callers
720
+ xflow deploy functions -m "…" send the project root, check it, apply the new
721
+ migrations, ship the functions and their callers
723
722
  xflow deploy apps/<folder> -m "…" send one app and build it; inside its folder a plain
724
723
  xflow deploy does the same
725
724
  xflow publish [build] point the app address at the newest build, or a named one
@@ -760,11 +759,13 @@ keys in the code, alert instead of a dialog) is printed as a list and nothing st
760
759
  merely unfinished (the template page title, no icon) goes through and prints a line
761
760
  ${(0, ui_1.bold)('Unfinished:')}. Fix it before telling the person the app is done.
762
761
 
763
- ${(0, ui_1.bold)('deploy functions')} bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
762
+ ${(0, ui_1.bold)('deploy functions')} first applies the new migrations of ${(0, ui_1.bold)('migrations/')}: if one fails or
763
+ would destroy data, nothing ships, and ${(0, ui_1.bold)('xflow db migrate')} is where that gets settled. Then it
764
+ bundles everything in ${(0, ui_1.bold)('functions/<name>/index.ts')}. A function whose
764
765
  code and variables did not change is left alone, and one that fails to ship fails the whole
765
766
  deploy. It then records the callers and prints which functions are open to the whole
766
- internet now. A change that touches data and screens goes in this order: the migration,
767
- the functions, then every app that shows it; an app deploy warns when the root holds
767
+ internet now. A change that touches data and screens goes in this order: the functions
768
+ (with the migration), then every app that shows it; an app deploy warns when the root holds
768
769
  changes nobody deployed.
769
770
 
770
771
  A function gone from the sources would be deleted from the cloud along with its keys and
@@ -774,9 +775,6 @@ to agree. If the sources hold no functions at all while the cloud holds several,
774
775
  is deleted and nothing is asked: that looks like a directory which never made it (a
775
776
  ${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
776
777
 
777
- The database is not part of this: migrations change data in ways nothing can undo, so
778
- they stay their own command (${(0, ui_1.bold)('xflow db migrate')}).
779
-
780
778
  A build of an app is not shown to visitors until ${(0, ui_1.bold)('xflow publish')}: publishing points the
781
779
  app address at a build and takes a second, and an older kept build is the rollback. Only
782
780
  the app moves: the functions and the database are one per project and stay as they are.
package/dist/template.js CHANGED
@@ -13,7 +13,7 @@ const README = `# %NAME%
13
13
  A project on XFlow: one database, cloud functions and the apps that use them.
14
14
 
15
15
  \`\`\`
16
- xflow.json projectId, and which apps and pages may call each function
16
+ xflow.json projectId, and which apps may call each function
17
17
  package.json dependencies of the functions
18
18
  functions/<name>/ cloud functions, shared by every app
19
19
  migrations/ the database of the project
package/dist/version.js CHANGED
@@ -2,6 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_API_URL = exports.CLI_VERSION = void 0;
4
4
  /** Keep in sync with cli/package.json. */
5
- exports.CLI_VERSION = '1.0.9';
5
+ exports.CLI_VERSION = '1.1.1';
6
6
  /** Overridden by XFLOW_API_URL or the `api` field in xflow.json. */
7
7
  exports.DEFAULT_API_URL = 'https://app.getxflow.com';
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
- {
2
- "name": "@getxflow/cli",
3
- "version": "1.0.9",
4
- "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
- "license": "UNLICENSED",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "bin": {
10
- "xflow": "dist/bin.js"
11
- },
12
- "files": [
13
- "dist",
14
- "skills"
15
- ],
16
- "scripts": {
17
- "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
18
- "build": "npm run clean && tsc -p tsconfig.json",
19
- "prepublishOnly": "npm run build"
20
- },
21
- "publishConfig": {
22
- "access": "public"
23
- }
24
- }
1
+ {
2
+ "name": "@getxflow/cli",
3
+ "version": "1.1.1",
4
+ "description": "CLI for the XFlow platform: projects with apps, functions and a database; source sync, deployment and publishing",
5
+ "license": "UNLICENSED",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "bin": {
10
+ "xflow": "dist/bin.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "skills"
15
+ ],
16
+ "scripts": {
17
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
18
+ "build": "npm run clean && tsc -p tsconfig.json",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }