@getxflow/cli 0.4.0 → 0.5.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 +1 -0
- package/dist/api.js +5 -2
- package/dist/args.js +1 -0
- package/dist/bin.js +9 -1
- package/dist/commands/db.js +80 -0
- package/dist/commands/deploy.js +40 -14
- package/dist/help.js +33 -15
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +28 -10
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ else's code belongs next to it.
|
|
|
21
21
|
| `status` / `push` / `pull` | state, sending and fetching sources |
|
|
22
22
|
| `deploy` / `publish` / `rollback` / `deployments` | build, publish, roll back, version history |
|
|
23
23
|
| `db status` / `db migrate` | migrations from `migrations/*.sql` with a gate on destructive ones |
|
|
24
|
+
| `db schema` / `db query` | tables and columns, reading data in a read-only transaction |
|
|
24
25
|
| `functions list` | cloud functions of the project from `functions/<name>/index.ts`, shipped by `deploy` |
|
|
25
26
|
| `functions invoke` / `functions logs` | call a function, look at its crashes with the stack |
|
|
26
27
|
| `schedules list` / `set` / `rm` | running functions on a timer (timer triggers) |
|
package/dist/api.js
CHANGED
|
@@ -14,7 +14,9 @@ class ApiError extends Error {
|
|
|
14
14
|
issues;
|
|
15
15
|
/** Set when a plan limit was hit. */
|
|
16
16
|
limit;
|
|
17
|
-
|
|
17
|
+
/** Cloud functions a build would delete, with code `confirm_required`. */
|
|
18
|
+
removing;
|
|
19
|
+
constructor(message, code, status, hint, issues, limit, removing) {
|
|
18
20
|
super(message);
|
|
19
21
|
this.name = 'ApiError';
|
|
20
22
|
this.code = code;
|
|
@@ -22,6 +24,7 @@ class ApiError extends Error {
|
|
|
22
24
|
this.hint = hint;
|
|
23
25
|
this.issues = issues;
|
|
24
26
|
this.limit = limit;
|
|
27
|
+
this.removing = removing;
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
exports.ApiError = ApiError;
|
|
@@ -90,7 +93,7 @@ async function readError(response) {
|
|
|
90
93
|
const text = await response.text().catch(() => '');
|
|
91
94
|
try {
|
|
92
95
|
const parsed = JSON.parse(text);
|
|
93
|
-
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues, parsed.limit);
|
|
96
|
+
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed.issues, parsed.limit, parsed.removing);
|
|
94
97
|
}
|
|
95
98
|
catch {
|
|
96
99
|
return new ApiError(`Request rejected (${response.status})`, 'unknown', response.status, text.slice(0, 200) || undefined);
|
package/dist/args.js
CHANGED
package/dist/bin.js
CHANGED
|
@@ -122,11 +122,19 @@ async function run(args) {
|
|
|
122
122
|
await (0, db_1.dbMigrate)(rest);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
|
+
if (second === 'schema') {
|
|
126
|
+
await (0, db_1.dbSchema)({ ...args, words: args.words.slice(2) });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (second === 'query') {
|
|
130
|
+
await (0, db_1.dbQuery)({ ...args, words: args.words.slice(2) });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
125
133
|
if (second === undefined || second === 'status') {
|
|
126
134
|
await (0, db_1.dbStatus)();
|
|
127
135
|
return;
|
|
128
136
|
}
|
|
129
|
-
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status and migrate');
|
|
137
|
+
throw new errors_1.CliError(`Unknown command: db ${second}`, 'Available: status, schema, query and migrate');
|
|
130
138
|
case 'status':
|
|
131
139
|
await (0, sources_1.status)();
|
|
132
140
|
return;
|
package/dist/commands/db.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dbStatus = dbStatus;
|
|
4
|
+
exports.dbSchema = dbSchema;
|
|
5
|
+
exports.dbQuery = dbQuery;
|
|
4
6
|
exports.dbMigrate = dbMigrate;
|
|
5
7
|
const node_fs_1 = require("node:fs");
|
|
6
8
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -74,6 +76,84 @@ async function dbStatus() {
|
|
|
74
76
|
(0, ui_1.warn)('A changed file will not be applied again: write a new migration');
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
/** Long values would tear the columns apart; the cut is marked, never silent. */
|
|
80
|
+
const CELL_LIMIT = 60;
|
|
81
|
+
function cell(value) {
|
|
82
|
+
if (value === null || value === undefined)
|
|
83
|
+
return '-';
|
|
84
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
85
|
+
return text.length > CELL_LIMIT ? `${text.slice(0, CELL_LIMIT - 1)}…` : text;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What the database looks like right now.
|
|
89
|
+
*
|
|
90
|
+
* Not the same question as the migrations/ directory: one logical database is
|
|
91
|
+
* shared by several projects, so the files answer "what did I do", not "what is
|
|
92
|
+
* in there".
|
|
93
|
+
*/
|
|
94
|
+
async function dbSchema(args) {
|
|
95
|
+
const { config } = (0, config_1.requireProject)();
|
|
96
|
+
const client = (0, session_1.connect)(config);
|
|
97
|
+
const wanted = args.words[0];
|
|
98
|
+
const path = `/api/v1/projects/${config.projectId}/db/schema${wanted ? `?table=${encodeURIComponent(wanted)}` : ''}`;
|
|
99
|
+
if (wanted) {
|
|
100
|
+
const data = await (0, api_1.apiJson)(client, path);
|
|
101
|
+
(0, ui_1.out)(`${(0, ui_1.bold)(`${data.schema}.${data.table}`)}`);
|
|
102
|
+
(0, ui_1.out)('');
|
|
103
|
+
(0, ui_1.table)(data.columns.map((column) => [
|
|
104
|
+
column.column_name,
|
|
105
|
+
column.data_type,
|
|
106
|
+
column.is_nullable ? 'null' : 'not null',
|
|
107
|
+
[
|
|
108
|
+
column.is_primary_key ? 'primary key' : '',
|
|
109
|
+
column.is_foreign_key ? `→ ${column.foreign_key_ref ?? 'foreign key'}` : '',
|
|
110
|
+
]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join(' '),
|
|
113
|
+
]));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const data = await (0, api_1.apiJson)(client, path);
|
|
117
|
+
(0, ui_1.out)(`Schema: ${(0, ui_1.bold)(data.schema)}`);
|
|
118
|
+
(0, ui_1.out)('');
|
|
119
|
+
if (data.tables.length === 0) {
|
|
120
|
+
(0, ui_1.note)('No tables yet. The first migration: migrations/0001_init.sql');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
(0, ui_1.table)(data.tables.map((row) => [
|
|
124
|
+
row.table_name,
|
|
125
|
+
row.row_count === null ? '' : `~${row.row_count} rows`,
|
|
126
|
+
row.size ?? '',
|
|
127
|
+
]));
|
|
128
|
+
(0, ui_1.note)((0, ui_1.dim)(' Columns of one table: xflow db schema <table>'));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Reading data.
|
|
132
|
+
*
|
|
133
|
+
* The query runs inside a READ ONLY transaction, so writes are rejected by the
|
|
134
|
+
* database itself rather than by us guessing at the text of the SQL.
|
|
135
|
+
*/
|
|
136
|
+
async function dbQuery(args) {
|
|
137
|
+
const { config } = (0, config_1.requireProject)();
|
|
138
|
+
const sql = args.words.join(' ').trim();
|
|
139
|
+
if (!sql) {
|
|
140
|
+
throw new errors_1.CliError('A query is required', 'For example: xflow db query "select * from orders order by created_at desc limit 20"');
|
|
141
|
+
}
|
|
142
|
+
const client = (0, session_1.connect)(config);
|
|
143
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/db/query`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
body: { sql, limit: (0, args_1.flagNumber)(args, 'limit') },
|
|
146
|
+
timeoutMs: 60_000,
|
|
147
|
+
});
|
|
148
|
+
if (data.rows.length === 0) {
|
|
149
|
+
(0, ui_1.note)('No rows');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
(0, ui_1.table)([data.columns, ...data.rows.map((row) => data.columns.map((column) => cell(row[column])))]);
|
|
153
|
+
if (data.truncated) {
|
|
154
|
+
(0, ui_1.note)((0, ui_1.dim)(` Shown ${data.rows.length} of ${data.total}: narrow the query or raise --limit`));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
77
157
|
async function dbMigrate(args) {
|
|
78
158
|
const { root, config } = (0, config_1.requireProject)();
|
|
79
159
|
const client = (0, session_1.connect)(config);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -10,6 +10,7 @@ const api_1 = require("../api");
|
|
|
10
10
|
const args_1 = require("../args");
|
|
11
11
|
const config_1 = require("../config");
|
|
12
12
|
const errors_1 = require("../errors");
|
|
13
|
+
const prompt_1 = require("../prompt");
|
|
13
14
|
const session_1 = require("../session");
|
|
14
15
|
const template_1 = require("../template");
|
|
15
16
|
const ui_1 = require("../ui");
|
|
@@ -61,6 +62,44 @@ async function refreshFunctionsEnv(root, client, projectId) {
|
|
|
61
62
|
}
|
|
62
63
|
return card.functions.filter((fn) => fn.invoke_url).map((fn) => fn.name);
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Start the build, asking before anything is destroyed.
|
|
67
|
+
*
|
|
68
|
+
* Removing a cloud function cannot be undone: one created again later gets a
|
|
69
|
+
* different address, and its schedules go with it. The platform therefore
|
|
70
|
+
* refuses such a build until the caller says yes, and the question is asked
|
|
71
|
+
* here, before the build starts, rather than reported once it already has.
|
|
72
|
+
*/
|
|
73
|
+
async function startBuild(client, projectId, revision, allowRemovals) {
|
|
74
|
+
const path = `/api/v1/projects/${projectId}/builds`;
|
|
75
|
+
try {
|
|
76
|
+
return await (0, api_1.apiJson)(client, path, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
body: { revision, allow_removals: allowRemovals },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
if (e instanceof api_1.ApiError && e.issues?.length)
|
|
83
|
+
reportIssues(e);
|
|
84
|
+
const removing = e instanceof api_1.ApiError && e.code === 'confirm_required' ? (e.removing ?? []) : [];
|
|
85
|
+
// No terminal (CI, an agent): the flag is the only way to say yes, and the
|
|
86
|
+
// platform message already names it.
|
|
87
|
+
if (removing.length === 0 || process.stdin.isTTY !== true || process.stderr.isTTY !== true)
|
|
88
|
+
throw e;
|
|
89
|
+
(0, ui_1.warn)(`Gone from the sources, the build would remove them: ${removing.join(', ')}`);
|
|
90
|
+
(0, ui_1.note)((0, ui_1.dim)(' Removal is final: a function created again later gets a different address, schedules included'));
|
|
91
|
+
const answer = await (0, prompt_1.select)('Remove them from the cloud?', [
|
|
92
|
+
{ label: 'No, stop here', hint: 'bring the functions/<name>/index.ts directories back' },
|
|
93
|
+
{ label: 'Yes, remove and build', hint: removing.join(', ') },
|
|
94
|
+
]);
|
|
95
|
+
if (answer !== 1)
|
|
96
|
+
throw new errors_1.CliError('The build did not start', 'Nothing was removed');
|
|
97
|
+
return (0, api_1.apiJson)(client, path, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
body: { revision, allow_removals: true },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
64
103
|
async function deploy(args) {
|
|
65
104
|
const { root, config } = (0, config_1.requireProject)();
|
|
66
105
|
const client = (0, session_1.connect)(config);
|
|
@@ -77,20 +116,7 @@ async function deploy(args) {
|
|
|
77
116
|
revision = (await (0, sources_1.pushSources)(root, config, client, { force: (0, args_1.flagBool)(args, 'force') })).revision;
|
|
78
117
|
}
|
|
79
118
|
(0, ui_1.step)(`Building on the platform from revision ${revision}`);
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
started = await (0, api_1.apiJson)(client, `/api/v1/projects/${config.projectId}/builds`, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
body: { revision },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (e) {
|
|
88
|
-
if (e instanceof api_1.ApiError && e.issues?.length)
|
|
89
|
-
reportIssues(e);
|
|
90
|
-
throw e;
|
|
91
|
-
}
|
|
92
|
-
// Removing a function cannot be undone: a new one gets a new address. Say it
|
|
93
|
-
// out loud rather than leaving it to be discovered in functions list.
|
|
119
|
+
const started = await startBuild(client, config.projectId, revision, (0, args_1.flagBool)(args, 'allow-removals'));
|
|
94
120
|
if (started.removed_functions?.length) {
|
|
95
121
|
(0, ui_1.warn)(`Removed from the cloud, gone from the sources: ${started.removed_functions.join(', ')}`);
|
|
96
122
|
}
|
package/dist/help.js
CHANGED
|
@@ -46,6 +46,8 @@ ${(0, ui_1.bold)('Functions')}
|
|
|
46
46
|
|
|
47
47
|
${(0, ui_1.bold)('Database')}
|
|
48
48
|
xflow db status which migrations are applied and which are waiting
|
|
49
|
+
xflow db schema [table] tables of the project, or the columns of one
|
|
50
|
+
xflow db query "select ..." read data, in a read-only transaction
|
|
49
51
|
xflow db migrate [--dry-run] [--allow-destructive]
|
|
50
52
|
apply the migrations from migrations/*.sql
|
|
51
53
|
|
|
@@ -63,22 +65,35 @@ ${(0, ui_1.bold)('Environment')}
|
|
|
63
65
|
More about one command: xflow help <command>`);
|
|
64
66
|
}
|
|
65
67
|
const TOPICS = {
|
|
66
|
-
db: `${(0, ui_1.bold)('xflow db
|
|
68
|
+
db: `${(0, ui_1.bold)('xflow db')}: schema, data, migrations
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
xflow db status what is applied and what is waiting
|
|
71
|
+
xflow db schema [table] tables of the project, or the columns of one
|
|
72
|
+
xflow db query "select ..." read data (--limit N)
|
|
73
|
+
xflow db migrate apply migrations/*.sql
|
|
74
|
+
|
|
75
|
+
${(0, ui_1.bold)('schema')} answers a different question than the ${(0, ui_1.bold)('migrations/')} directory: one logical
|
|
76
|
+
database is shared by several projects, so the files say what you did, and the schema
|
|
77
|
+
says what is actually in there. ${(0, ui_1.bold)('query')} runs inside a READ ONLY transaction, so
|
|
78
|
+
writes are rejected by the database itself, not by us reading your SQL.
|
|
79
|
+
|
|
80
|
+
${(0, ui_1.bold)('migrate')}: the files live in the repository (${(0, ui_1.bold)('migrations/0001_init.sql')},
|
|
81
|
+
${(0, ui_1.bold)('migrations/0002_orders.sql')} and so on). The order comes from the file name, which
|
|
82
|
+
is why the leading number is required. Every migration runs in its own transaction, the
|
|
83
|
+
history is kept in the database itself, and anything already applied is not run again.
|
|
72
84
|
|
|
73
85
|
--dry-run report what would be applied without touching the database
|
|
74
86
|
--allow-destructive allow operations that destroy data
|
|
75
87
|
|
|
76
88
|
About destructive ones. The platform keeps no database history and makes no backups,
|
|
77
89
|
so ${(0, ui_1.bold)('DROP TABLE')}, ${(0, ui_1.bold)('DROP COLUMN')}, ${(0, ui_1.bold)('TRUNCATE')} and ${(0, ui_1.bold)('DELETE FROM')} without a
|
|
78
|
-
condition
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
is
|
|
90
|
+
condition need two things at once: this flag, and the right to destroy data on the
|
|
91
|
+
access key. That right is off by default, and only its owner turns it on, in the
|
|
92
|
+
platform settings under Developers: a flag is something an agent adds by itself, a
|
|
93
|
+
right is not. With both in place, the contents of the affected tables are dumped and
|
|
94
|
+
kept for 7 days: that is a "caught it right away" safety net, not a backup.
|
|
95
|
+
${(0, ui_1.bold)('DROP DATABASE')} is never allowed, because the database is shared across the
|
|
96
|
+
organization.
|
|
82
97
|
|
|
83
98
|
Editing an already applied file achieves nothing: the comparison is by name, not by
|
|
84
99
|
content. ${(0, ui_1.bold)('xflow db status')} lists such a file separately, so write a new migration
|
|
@@ -233,8 +248,9 @@ Three steps: sending the sources, shipping the cloud functions, building the app
|
|
|
233
248
|
The build command and the output directory come from xflow.json (npm run build and dist
|
|
234
249
|
by default).
|
|
235
250
|
|
|
236
|
-
--no-push
|
|
237
|
-
--force
|
|
251
|
+
--no-push do not send sources, build from the latest server revision
|
|
252
|
+
--force allow overwriting the server revision while sending
|
|
253
|
+
--allow-removals agree in advance to remove the functions gone from the sources
|
|
238
254
|
|
|
239
255
|
The platform builds, in a clean sandbox on one Node version for everybody, so "it
|
|
240
256
|
worked on my machine" no longer depends on your machine. Before the build the project
|
|
@@ -247,10 +263,12 @@ works: the addresses of the functions are baked into the bundle, so they have to
|
|
|
247
263
|
first. A function whose code and variables did not change is left alone, and a function
|
|
248
264
|
that fails to ship fails the whole build.
|
|
249
265
|
|
|
250
|
-
A function gone from the sources
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
266
|
+
A function gone from the sources would be deleted from the cloud along with its
|
|
267
|
+
schedules, and that is final: one created again later gets a different address. So the
|
|
268
|
+
build does not start at all until you say yes. In a terminal the CLI names the functions
|
|
269
|
+
and asks; without one (CI, an agent) it stops and ${(0, ui_1.bold)('--allow-removals')} is the only way
|
|
270
|
+
to agree. If the sources hold no functions at all while the cloud holds several, nothing
|
|
271
|
+
is deleted and nothing is asked: that looks like a directory which never made it (a
|
|
254
272
|
${(0, ui_1.bold)('functions/')} line in .xflowignore) rather than a decision.
|
|
255
273
|
|
|
256
274
|
The database is not part of this: migrations change data in ways nothing can undo, so
|
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 = '0.
|
|
5
|
+
exports.CLI_VERSION = '0.5.0';
|
|
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
package/skills/xflow/SKILL.md
CHANGED
|
@@ -68,7 +68,9 @@ restored within an hour of the data going back under the limit.
|
|
|
68
68
|
6. `xflow publish` makes that same version visible to visitors.
|
|
69
69
|
|
|
70
70
|
The split is deliberate: shipping a build and showing it are two separate decisions.
|
|
71
|
-
Until `publish` runs, visitors keep seeing the previous version.
|
|
71
|
+
Until `publish` runs, visitors keep seeing the previous version. The one exception is
|
|
72
|
+
the very first version of a project: it publishes automatically, since there is no
|
|
73
|
+
live version to protect yet.
|
|
72
74
|
|
|
73
75
|
Rolling back: `xflow deployments` lists the version history, `xflow rollback <id>`
|
|
74
76
|
points the project back at an earlier build. Sources stay on their own revision.
|
|
@@ -162,10 +164,17 @@ on every call. Branch the frontend on `error.code`, never on `error.message`: wo
|
|
|
162
164
|
gets rewritten on any edit, a code does not.
|
|
163
165
|
|
|
164
166
|
The sources are the whole truth about which functions exist. Delete the directory and the
|
|
165
|
-
next deploy
|
|
167
|
+
next deploy would delete the function from the cloud, schedules included, and that cannot be
|
|
166
168
|
undone: a function created again later gets a different address. So never remove a function
|
|
167
169
|
directory to "clean up" unless the user asked for the function to go.
|
|
168
170
|
|
|
171
|
+
Such a deploy does not start on its own: the platform names the functions it would remove and
|
|
172
|
+
refuses until somebody agrees. Under an agent there is no terminal to ask in, so the refusal
|
|
173
|
+
reaches you, and `--allow-removals` is the only way past it. Adding that flag to get the
|
|
174
|
+
build running is exactly the wrong move: it means you deleted something the user did not ask
|
|
175
|
+
you to delete. Put the directories back instead, and if the removal really is intended, say
|
|
176
|
+
which functions are about to go and let the user answer.
|
|
177
|
+
|
|
169
178
|
Debugging a deployed function is two commands: `xflow functions invoke <name>` calls it
|
|
170
179
|
the way the app does and prints status, timing and body (`--data '{"a":1}'` sends a body),
|
|
171
180
|
and `xflow functions logs <name>` shows the failures, each with its stack and the console
|
|
@@ -276,13 +285,23 @@ under one of them would shadow the real one.
|
|
|
276
285
|
|
|
277
286
|
The platform keeps no database history and no backups. Anything that destroys data
|
|
278
287
|
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) is refused
|
|
279
|
-
unless you pass `--allow-destructive`, and
|
|
280
|
-
|
|
288
|
+
unless two things hold at once: you pass `--allow-destructive`, and the access key carries
|
|
289
|
+
the right to destroy data. That right is off by default and only its owner can turn it on,
|
|
290
|
+
in the platform settings, under Developers. So when a destructive migration is refused for
|
|
291
|
+
the right rather than the flag, adding the flag changes nothing: say what needs deleting and
|
|
292
|
+
why, and let the person decide. With both in place the affected tables are dumped first and
|
|
293
|
+
kept for 7 days. Check with `--dry-run` before applying.
|
|
281
294
|
|
|
282
295
|
One logical database can be shared by several projects, so your migration can break an app
|
|
283
296
|
you do not see. `xflow db status` lists applied migrations that have no file in your
|
|
284
297
|
repository: that is what someone else's project did.
|
|
285
298
|
|
|
299
|
+
For the same reason the `migrations/` directory is not the schema. It says what you did;
|
|
300
|
+
`xflow db schema` says what is in the database right now, and `xflow db schema <table>` gives
|
|
301
|
+
the columns of one table. `xflow db query "select ..."` reads data, inside a READ ONLY
|
|
302
|
+
transaction, so a write there fails by design rather than by accident. Look before you write
|
|
303
|
+
a migration against a shared database.
|
|
304
|
+
|
|
286
305
|
## File storage
|
|
287
306
|
|
|
288
307
|
The project has file storage, and the browser cannot reach it. Those endpoints take only the
|
|
@@ -356,12 +375,11 @@ read-only queries, migrations, function logs and invocations, schedules, environ
|
|
|
356
375
|
variables, versions, publish and rollback. They answer with aggregates and say explicitly
|
|
357
376
|
when a result is truncated, which parsing terminal output does not.
|
|
358
377
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
server
|
|
363
|
-
|
|
364
|
-
minutes, so never expect the starting call to return a finished version.
|
|
378
|
+
Anything that depends on the working copy stays in the CLI: sending sources (`xflow push`),
|
|
379
|
+
building and shipping the functions (`xflow deploy`), creating a project (`xflow init`). The
|
|
380
|
+
tools cannot see the folder you are working in, so a build started from there would release
|
|
381
|
+
whatever revision the server happens to hold, not what you have on disk. Pulling a repository
|
|
382
|
+
through tool calls also burns the user's tokens for nothing.
|
|
365
383
|
|
|
366
384
|
## Do not
|
|
367
385
|
|