@getxflow/cli 0.14.0 → 0.14.2
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/api.js +7 -8
- package/dist/commands/connections.js +38 -29
- package/dist/flags.js +34 -6
- package/dist/help.js +7 -5
- package/dist/json-arg.js +24 -11
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +10 -9
package/dist/api.js
CHANGED
|
@@ -12,21 +12,20 @@ class ApiError extends Error {
|
|
|
12
12
|
code;
|
|
13
13
|
status;
|
|
14
14
|
hint;
|
|
15
|
-
/** Check violations, e.g. why a build was rejected. */
|
|
16
15
|
issues;
|
|
17
|
-
/** Set when a plan limit was hit. */
|
|
18
16
|
limit;
|
|
19
|
-
/** Cloud functions a build would delete, with code `confirm_required`. */
|
|
20
17
|
removing;
|
|
21
|
-
|
|
18
|
+
request;
|
|
19
|
+
constructor(message, code, status, hint, details = {}) {
|
|
22
20
|
super(message);
|
|
23
21
|
this.name = 'ApiError';
|
|
24
22
|
this.code = code;
|
|
25
23
|
this.status = status;
|
|
26
24
|
this.hint = hint;
|
|
27
|
-
this.issues = issues;
|
|
28
|
-
this.limit = limit;
|
|
29
|
-
this.removing = removing;
|
|
25
|
+
this.issues = details.issues;
|
|
26
|
+
this.limit = details.limit;
|
|
27
|
+
this.removing = details.removing;
|
|
28
|
+
this.request = details.request;
|
|
30
29
|
}
|
|
31
30
|
}
|
|
32
31
|
exports.ApiError = ApiError;
|
|
@@ -127,7 +126,7 @@ async function readError(response, client) {
|
|
|
127
126
|
const text = await response.text().catch(() => '');
|
|
128
127
|
try {
|
|
129
128
|
const parsed = JSON.parse(text);
|
|
130
|
-
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed
|
|
129
|
+
return new ApiError(parsed.error || `Request rejected (${response.status})`, parsed.code || 'unknown', response.status, parsed.hint, parsed);
|
|
131
130
|
}
|
|
132
131
|
catch {
|
|
133
132
|
return notAnAnswer(response.status, client.apiUrl, text);
|
|
@@ -164,9 +164,6 @@ function head(text) {
|
|
|
164
164
|
/**
|
|
165
165
|
* Response headers worth a line each: rate limits, the type, and where a redirect points.
|
|
166
166
|
* A CDN adds two dozen more, and on every call that is noise.
|
|
167
|
-
*
|
|
168
|
-
* Request headers are not filtered: there are a few, they are yours, and seeing them is how
|
|
169
|
-
* a `{NAME}` the connection does not have shows itself.
|
|
170
167
|
*/
|
|
171
168
|
function shownHeaders(headers) {
|
|
172
169
|
const wanted = (name) => name === 'content-type' || name === 'retry-after' || name === 'location' || name.startsWith('x-ratelimit');
|
|
@@ -174,8 +171,18 @@ function shownHeaders(headers) {
|
|
|
174
171
|
.filter(([name]) => wanted(name.toLowerCase()))
|
|
175
172
|
.map(([name, value]) => `${name}: ${value}`);
|
|
176
173
|
}
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
/**
|
|
175
|
+
* The request as it went out, printed on the answer and on a refusal alike. On a refusal it
|
|
176
|
+
* is the whole point: the host of a connector comes from the connection, not from what was
|
|
177
|
+
* typed, so "check the address" is unfollowable while the address is nowhere on screen.
|
|
178
|
+
*
|
|
179
|
+
* Request headers are not filtered: there are a few, they are yours, and seeing them is how
|
|
180
|
+
* a {NAME} the connection does not have shows itself.
|
|
181
|
+
*/
|
|
182
|
+
function showRequest(request) {
|
|
183
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${request.method} ${request.url}`));
|
|
184
|
+
for (const [name, value] of Object.entries(request.headers))
|
|
185
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${name}: ${value}`));
|
|
179
186
|
}
|
|
180
187
|
/** Headers typed as JSON. Values may carry {NAME} placeholders, the platform fills those in. */
|
|
181
188
|
function headerArg(args) {
|
|
@@ -223,28 +230,32 @@ async function connectionsCall(args) {
|
|
|
223
230
|
const body = (0, json_arg_1.bodyArg)(args, 'data');
|
|
224
231
|
// Judged before anything goes over the network, the same as in functions invoke: a broken
|
|
225
232
|
// body is the caller's own line. A form body is not judged at all, it is text by design.
|
|
226
|
-
if (body && looksJson(headers))
|
|
227
|
-
|
|
228
|
-
if (problem) {
|
|
229
|
-
throw body.fromFile
|
|
230
|
-
? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
|
|
231
|
-
: new errors_1.CliError(`--data is not valid JSON: ${body.text}`, (0, json_arg_1.jsonHint)('data', body.text));
|
|
232
|
-
}
|
|
233
|
-
}
|
|
233
|
+
if (body && looksJson(headers))
|
|
234
|
+
(0, json_arg_1.assertJson)('data', body);
|
|
234
235
|
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
236
|
+
let data;
|
|
237
|
+
try {
|
|
238
|
+
data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/connections/call`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
body: {
|
|
241
|
+
connection,
|
|
242
|
+
path,
|
|
243
|
+
method: (0, args_1.flagString)(args, 'method') ?? (body ? 'POST' : 'GET'),
|
|
244
|
+
...(body ? { body: body.text } : {}),
|
|
245
|
+
...(headers ? { headers } : {}),
|
|
246
|
+
},
|
|
247
|
+
// Past the platform's own cap on the call, so a slow API reads as a slow API and not as
|
|
248
|
+
// an unreachable platform.
|
|
249
|
+
timeoutMs: 60_000,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
// Before the refusal itself, in the order of the answer: what went out, then what came of
|
|
254
|
+
// it. Only the other side's refusals carry it, the rest never assembled an address.
|
|
255
|
+
if (e instanceof api_1.ApiError && e.request)
|
|
256
|
+
showRequest(e.request);
|
|
257
|
+
throw e;
|
|
258
|
+
}
|
|
248
259
|
if ((0, args_1.flagBool)(args, 'json')) {
|
|
249
260
|
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
250
261
|
return;
|
|
@@ -254,9 +265,7 @@ async function connectionsCall(args) {
|
|
|
254
265
|
// service under the same name, the alias is the only thing the caller already knew.
|
|
255
266
|
if (!data.alias)
|
|
256
267
|
(0, ui_1.note)((0, ui_1.dim)(` ${data.label} (${data.connection_id}), not linked to this project`));
|
|
257
|
-
(
|
|
258
|
-
for (const line of allHeaders(request.headers))
|
|
259
|
-
(0, ui_1.note)((0, ui_1.dim)(` ${line}`));
|
|
268
|
+
showRequest(request);
|
|
260
269
|
const size = response.truncated ? `at least ${(0, ui_1.formatBytes)(response.bytes)}` : (0, ui_1.formatBytes)(response.bytes);
|
|
261
270
|
(0, ui_1.note)((0, ui_1.dim)(` ${response.status} in ${data.elapsed_ms} ms, ${size}`));
|
|
262
271
|
for (const line of shownHeaders(response.headers))
|
package/dist/flags.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.KNOWN = void 0;
|
|
3
|
+
exports.WORKING_COPY = exports.KNOWN = void 0;
|
|
4
4
|
exports.checkFlags = checkFlags;
|
|
5
5
|
const args_1 = require("./args");
|
|
6
6
|
const errors_1 = require("./errors");
|
|
@@ -19,8 +19,8 @@ const help_1 = require("./help");
|
|
|
19
19
|
*
|
|
20
20
|
* `--project` is a flag like any other here, and it is written against a command
|
|
21
21
|
* only when that command can answer without the working copy. The ones that cannot
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* have a folder to stand in, and naming a project they cannot read would be a lie;
|
|
23
|
+
* they are listed in WORKING_COPY below, each with what it reads from that folder.
|
|
24
24
|
* `db status` sits halfway: named by id it answers with the applied history alone,
|
|
25
25
|
* without the comparison against migrations/.
|
|
26
26
|
*
|
|
@@ -87,6 +87,27 @@ exports.KNOWN = {
|
|
|
87
87
|
deployments: ['project'],
|
|
88
88
|
update: [],
|
|
89
89
|
};
|
|
90
|
+
/**
|
|
91
|
+
* What each of those commands reads from the working copy, in its own words.
|
|
92
|
+
*
|
|
93
|
+
* `has no flag --project` alone leaves the reader one dead end short of the answer:
|
|
94
|
+
* the next try, without the flag, ends at the missing xflow.json, and neither line
|
|
95
|
+
* tells a rule from an oversight. Naming the file the command would have to read
|
|
96
|
+
* does, and it also says when the errand needs no folder at all.
|
|
97
|
+
*
|
|
98
|
+
* Keys are the commands KNOWN keeps `--project` from, and flags.test.ts holds the
|
|
99
|
+
* two lists against each other: a command that later gains the flag has to lose its
|
|
100
|
+
* line here, or a refusal nobody prints any more stays behind as a lie.
|
|
101
|
+
*/
|
|
102
|
+
exports.WORKING_COPY = {
|
|
103
|
+
deploy: 'it sends the sources next to it',
|
|
104
|
+
pull: 'it writes the sources next to it',
|
|
105
|
+
status: 'it compares the folder with the revision on the platform',
|
|
106
|
+
'db migrate': 'it applies the files of migrations/',
|
|
107
|
+
'env check': 'it looks for the variables the code of the functions reads',
|
|
108
|
+
'env set': 'it names the functions that need a deploy for the value to arrive',
|
|
109
|
+
'connections unlink': 'it reads functions/ to name the functions that would lose the variables. Only trying an API out? That needs no link: xflow connections call',
|
|
110
|
+
};
|
|
90
111
|
/** Answered everywhere: both are handled before the command runs. */
|
|
91
112
|
const GLOBAL = new Set(['help', 'version']);
|
|
92
113
|
/**
|
|
@@ -118,6 +139,15 @@ function commandOf(words) {
|
|
|
118
139
|
}
|
|
119
140
|
return Object.hasOwn(exports.KNOWN, first) ? first : null;
|
|
120
141
|
}
|
|
142
|
+
function hintFor(command, flag, allowed, page) {
|
|
143
|
+
const reads = exports.WORKING_COPY[command];
|
|
144
|
+
if (flag === 'project' && reads !== undefined) {
|
|
145
|
+
return `It works in the project folder: ${reads}. Which commands take --project: xflow help project`;
|
|
146
|
+
}
|
|
147
|
+
return allowed.length > 0
|
|
148
|
+
? `It takes ${allowed.map((name) => `--${name}`).join(', ')}. What each one does: ${page}`
|
|
149
|
+
: `It takes no flags. What it does: ${page}`;
|
|
150
|
+
}
|
|
121
151
|
function checkFlags(args) {
|
|
122
152
|
const command = commandOf(args.words);
|
|
123
153
|
if (command === null)
|
|
@@ -131,9 +161,7 @@ function checkFlags(args) {
|
|
|
131
161
|
if (GLOBAL.has(name))
|
|
132
162
|
continue;
|
|
133
163
|
if (!allowed.includes(name)) {
|
|
134
|
-
throw new errors_1.CliError(`xflow ${command} has no flag --${name}`, allowed
|
|
135
|
-
? `It takes ${allowed.map((flag) => `--${flag}`).join(', ')}. What each one does: ${page}`
|
|
136
|
-
: `It takes no flags. What it does: ${page}`);
|
|
164
|
+
throw new errors_1.CliError(`xflow ${command} has no flag --${name}`, hintFor(command, name, allowed, page));
|
|
137
165
|
}
|
|
138
166
|
// The value went missing, so the flag arrived as a bare switch. Left alone it
|
|
139
167
|
// would mean the default, which is never what the person typing it wanted.
|
package/dist/help.js
CHANGED
|
@@ -254,7 +254,7 @@ About destructive ones. The platform keeps no database history and makes no back
|
|
|
254
254
|
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
|
|
255
255
|
condition need two things at once: this flag, and the right to destroy data on the
|
|
256
256
|
access key. That right is off by default, and only its owner turns it on, in the
|
|
257
|
-
platform settings
|
|
257
|
+
platform settings, where the key permissions live: a flag is something an agent adds by itself, a
|
|
258
258
|
right is not. With both in place, the contents of the affected tables are dumped and
|
|
259
259
|
kept for 7 days: that is a "caught it right away" safety net, not a backup.
|
|
260
260
|
${(0, ui_1.bold)('DROP DATABASE')} is never allowed, because the database is shared across the
|
|
@@ -321,8 +321,8 @@ Dot entries and symbolic links are skipped, and ${(0, ui_1.bold)('.xflowignore')
|
|
|
321
321
|
those are the rules for the sources, and a media folder is exactly what one is
|
|
322
322
|
asked to put there so it stays out of the archive.
|
|
323
323
|
|
|
324
|
-
${(0, ui_1.bold)('rm')} needs a right of its own (${(0, ui_1.bold)('Delete files')}
|
|
325
|
-
platform), off by default. The reason is the neighbourhood: whatever the users of
|
|
324
|
+
${(0, ui_1.bold)('rm')} needs a right of its own (${(0, ui_1.bold)('Delete files')}, where the key permissions live in
|
|
325
|
+
the platform settings), off by default. The reason is the neighbourhood: whatever the users of
|
|
326
326
|
the application uploaded lives in the same folders, and there is no undo. A folder
|
|
327
327
|
first answers with the number of files it holds and deletes only on ${(0, ui_1.bold)('--yes')}.
|
|
328
328
|
|
|
@@ -381,7 +381,9 @@ of the same service under one name; then the command prints their identifiers an
|
|
|
381
381
|
for one of those instead of guessing.
|
|
382
382
|
|
|
383
383
|
${(0, ui_1.bold)('unlink')} refuses while a function still reads one of the variables and names
|
|
384
|
-
those functions; ${(0, ui_1.bold)('--force')} goes through anyway.
|
|
384
|
+
those functions; ${(0, ui_1.bold)('--force')} goes through anyway. Seeing them needs the sources,
|
|
385
|
+
which is why ${(0, ui_1.bold)('unlink')} works in the project folder and takes no ${(0, ui_1.bold)('--project')}
|
|
386
|
+
while ${(0, ui_1.bold)('link')} takes it: only one of the two reads your code.
|
|
385
387
|
|
|
386
388
|
Variables are named after the alias: an OAuth connection called ${(0, ui_1.bold)('YANDEX_METRIKA')}
|
|
387
389
|
gives ${(0, ui_1.bold)('YANDEX_METRIKA_TOKEN')}, a key-based one gives a variable per field. They
|
|
@@ -390,7 +392,7 @@ run ${(0, ui_1.bold)('xflow deploy')}. A token close to expiry is renewed by any
|
|
|
390
392
|
a revoked one is not renewed by anything until a human reconnects the account.
|
|
391
393
|
|
|
392
394
|
Linking needs a right on the key, ${(0, ui_1.bold)('connections:link')}. Keys are issued with it,
|
|
393
|
-
and a person can take it away in the platform settings
|
|
395
|
+
and a person can take it away in the platform settings, where the key permissions live; a key cannot give
|
|
394
396
|
it back to itself. Losing it is what the refusal says, and the answer is to ask a person,
|
|
395
397
|
not to look for another route. Either way only the accounts granted to you personally can
|
|
396
398
|
be linked at all: that rule holds whatever the key is allowed to do.
|
package/dist/json-arg.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.jsonProblem = jsonProblem;
|
|
4
4
|
exports.jsonHint = jsonHint;
|
|
5
5
|
exports.bodyArg = bodyArg;
|
|
6
|
+
exports.assertJson = assertJson;
|
|
6
7
|
exports.jsonArg = jsonArg;
|
|
7
8
|
const node_fs_1 = require("node:fs");
|
|
8
9
|
const args_1 = require("./args");
|
|
@@ -30,15 +31,19 @@ function jsonProblem(raw) {
|
|
|
30
31
|
return e instanceof Error ? e.message : String(e);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Where to look, and on a stripped value the shell is the first suspect.
|
|
36
|
+
*
|
|
37
|
+
* No file is offered here: not every JSON flag has that twin, and --header used to be sent
|
|
38
|
+
* to --header-file, which the flag check refuses one line later. A hint has to survive being
|
|
39
|
+
* followed, so the offer lives in assertJson, the one path that reads a file.
|
|
40
|
+
*/
|
|
34
41
|
function jsonHint(flag, raw) {
|
|
35
|
-
const file = `--${flag}-file body.json`;
|
|
36
42
|
if (quotesStripped(raw.trim())) {
|
|
37
43
|
return (`Not one double quote survived: the shell removed them, PowerShell does this to ` +
|
|
38
|
-
`native calls. Escape them: --${flag} '{\\"key\\":\\"value\\"}'
|
|
39
|
-
`in a file: ${file}`);
|
|
44
|
+
`native calls. Escape them: --${flag} '{\\"key\\":\\"value\\"}'`);
|
|
40
45
|
}
|
|
41
|
-
return
|
|
46
|
+
return 'A single JSON value is expected';
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* The body from --<flag> or --<flag>-file, whatever it is.
|
|
@@ -65,15 +70,23 @@ function bodyArg(args, flag) {
|
|
|
65
70
|
throw new errors_1.CliError(`--${flag} needs a value`, `For example: --${flag} '{"mode":"full"}'`);
|
|
66
71
|
return { text: String(inline).trim(), fromFile: null };
|
|
67
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Refuse a body that is not JSON. Takes what bodyArg returned, and only this path names
|
|
75
|
+
* --<flag>-file: a flag read through bodyArg has that twin by construction.
|
|
76
|
+
*/
|
|
77
|
+
function assertJson(flag, body) {
|
|
78
|
+
const problem = jsonProblem(body.text);
|
|
79
|
+
if (!problem)
|
|
80
|
+
return;
|
|
81
|
+
throw body.fromFile
|
|
82
|
+
? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
|
|
83
|
+
: new errors_1.CliError(`--${flag} is not valid JSON: ${body.text}`, `${jsonHint(flag, body.text)}, or keep the body in a file: --${flag}-file body.json`);
|
|
84
|
+
}
|
|
68
85
|
/** The body from --<flag> or --<flag>-file, already known to be JSON. */
|
|
69
86
|
function jsonArg(args, flag) {
|
|
70
87
|
const body = bodyArg(args, flag);
|
|
71
88
|
if (body === undefined)
|
|
72
89
|
return undefined;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return body.text;
|
|
76
|
-
throw body.fromFile
|
|
77
|
-
? new errors_1.CliError(`${body.fromFile} is not valid JSON: ${problem}`, 'The file has to hold one JSON value')
|
|
78
|
-
: new errors_1.CliError(`--${flag} is not valid JSON: ${body.text}`, jsonHint(flag, body.text));
|
|
90
|
+
assertJson(flag, body);
|
|
91
|
+
return body.text;
|
|
79
92
|
}
|
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.14.
|
|
5
|
+
exports.CLI_VERSION = '0.14.2';
|
|
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
|
@@ -24,7 +24,7 @@ contains the fix.
|
|
|
24
24
|
|
|
25
25
|
## Keeping these instructions current
|
|
26
26
|
|
|
27
|
-
These instructions ship with xflow CLI 0.14.
|
|
27
|
+
These instructions ship with xflow CLI 0.14.2. They travel inside the package, so the copy
|
|
28
28
|
you are reading can be older than the CLI answering your commands, and nothing about that
|
|
29
29
|
is visible in the text itself.
|
|
30
30
|
|
|
@@ -360,13 +360,14 @@ unavailable: the account you need is often connected already, one link away.
|
|
|
360
360
|
|
|
361
361
|
`xflow connections link "Яндекс Метрика" --as YANDEX_METRIKA` is that link and
|
|
362
362
|
`xflow connections unlink YANDEX_METRIKA` undoes it; `xflow help connections` has the
|
|
363
|
-
naming rules and the flags. Unlink
|
|
364
|
-
|
|
363
|
+
naming rules and the flags. Unlink reads `functions/`, so it runs in the project folder:
|
|
364
|
+
it refuses while a function still reads one of the variables and names those functions,
|
|
365
|
+
so read that list before reaching for `--force`.
|
|
365
366
|
|
|
366
367
|
Linking needs the `connections:link` right on the key, and only accounts granted to the
|
|
367
368
|
owner of the key personally can be linked at all; the same right covers calling an unlinked
|
|
368
369
|
account. If the right was taken away, say so and
|
|
369
|
-
ask the person to turn it back on in the platform settings
|
|
370
|
+
ask the person to turn it back on where the key permissions live in the platform settings: a key cannot
|
|
370
371
|
grant it to itself. Connecting a new account and switching one off stay with a person too.
|
|
371
372
|
|
|
372
373
|
### Asking an API what it returns
|
|
@@ -388,7 +389,7 @@ one by its name: **linking first is not needed to look**, so do not ask a person
|
|
|
388
389
|
something. To use the account in code, link it and deploy.
|
|
389
390
|
|
|
390
391
|
Two refusals do need a person, and different ones: `connections:link` right — the owner of the
|
|
391
|
-
key grants it
|
|
392
|
+
key grants it where the key permissions live; `not granted to you` — ask whoever connected the account.
|
|
392
393
|
|
|
393
394
|
`{NAME}` in the path or in a header value is filled in from the fields of the connection: the
|
|
394
395
|
names are what `xflow env` shows without the alias in front, so `OZON_CLIENT_ID` is written
|
|
@@ -448,8 +449,8 @@ fills those in itself, and your value under one of them would shadow the real on
|
|
|
448
449
|
The platform keeps no database history and no backups, so anything that destroys data
|
|
449
450
|
(`DROP TABLE`, `DROP COLUMN`, `TRUNCATE`, `DELETE FROM` without a condition) needs two
|
|
450
451
|
things at once: the `--allow-destructive` flag, and the right to destroy data on the key.
|
|
451
|
-
That right is off by default and only its owner turns it on,
|
|
452
|
-
platform settings. When the refusal is about the right rather than the flag, adding the flag
|
|
452
|
+
That right is off by default and only its owner turns it on, where the key permissions live in
|
|
453
|
+
the platform settings. When the refusal is about the right rather than the flag, adding the flag
|
|
453
454
|
changes nothing: say what needs deleting and why, and let the person decide. With both in
|
|
454
455
|
place the affected tables are dumped first and kept for 7 days. Check with `--dry-run` first.
|
|
455
456
|
|
|
@@ -488,8 +489,8 @@ whose size differs. An address belongs to the record rather than to the bytes, s
|
|
|
488
489
|
replacement keeps it and the links in your code and tables keep working. Put those addresses
|
|
489
490
|
into the code or into a table: there is no command that fetches files back to a machine.
|
|
490
491
|
|
|
491
|
-
Deleting needs a right of its own, off by default (**Delete files
|
|
492
|
-
|
|
492
|
+
Deleting needs a right of its own, off by default (**Delete files**, where the key permissions
|
|
493
|
+
live in the platform settings), so `xflow storage rm` may answer that the key was not granted it.
|
|
493
494
|
Ask the person to switch it on, and do not look for a way around: whatever the users of the
|
|
494
495
|
application uploaded lives in the same folders, and there is no undo. A folder first tells
|
|
495
496
|
you how many files it holds and deletes them only with `--yes`.
|