@getxflow/cli 0.12.1 → 0.14.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/bin.js +5 -1
- package/dist/commands/connections.js +138 -0
- package/dist/flags.js +1 -0
- package/dist/help.js +42 -1
- package/dist/json-arg.js +23 -13
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/xflow/SKILL.md +38 -2
package/dist/bin.js
CHANGED
|
@@ -133,11 +133,15 @@ async function run(args) {
|
|
|
133
133
|
await (0, connections_1.connectionsUnlink)(rest);
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
136
|
+
if (second === 'call') {
|
|
137
|
+
await (0, connections_1.connectionsCall)(rest);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
136
140
|
if (second === undefined || second === 'list') {
|
|
137
141
|
await (0, connections_1.connectionsList)(rest);
|
|
138
142
|
return;
|
|
139
143
|
}
|
|
140
|
-
throw new errors_1.CliError(`Unknown command: connections ${second}`, 'Available: list, link and
|
|
144
|
+
throw new errors_1.CliError(`Unknown command: connections ${second}`, 'Available: list, link, unlink and call');
|
|
141
145
|
case 'schedules':
|
|
142
146
|
if (second === 'set') {
|
|
143
147
|
await (0, schedules_1.schedulesSet)(rest);
|
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.connectionsList = connectionsList;
|
|
4
4
|
exports.connectionsLink = connectionsLink;
|
|
5
5
|
exports.connectionsUnlink = connectionsUnlink;
|
|
6
|
+
exports.connectionsCall = connectionsCall;
|
|
6
7
|
const api_1 = require("../api");
|
|
7
8
|
const args_1 = require("../args");
|
|
8
9
|
const config_1 = require("../config");
|
|
9
10
|
const errors_1 = require("../errors");
|
|
11
|
+
const json_arg_1 = require("../json-arg");
|
|
10
12
|
const session_1 = require("../session");
|
|
11
13
|
const ui_1 = require("../ui");
|
|
12
14
|
const env_1 = require("./env");
|
|
@@ -135,3 +137,139 @@ async function connectionsUnlink(args) {
|
|
|
135
137
|
(0, ui_1.ok)(`${(0, ui_1.bold)(row.label)} unlinked, ${result.alias} is gone`);
|
|
136
138
|
(0, ui_1.note)((0, ui_1.dim)(' Functions already deployed keep the values until their next deploy'));
|
|
137
139
|
}
|
|
140
|
+
/** Budget for the shown body. Enough to read the shape of an answer, cheap in context. */
|
|
141
|
+
const SHOWN_CHARS = 8000;
|
|
142
|
+
/**
|
|
143
|
+
* Head of the body, cut on a line boundary so JSON is never sliced mid-structure. Only the
|
|
144
|
+
* head is kept: a stitched head and tail would read as one answer and invite conclusions the
|
|
145
|
+
* data does not support.
|
|
146
|
+
*/
|
|
147
|
+
function head(text) {
|
|
148
|
+
if (text.length <= SHOWN_CHARS)
|
|
149
|
+
return { text, cut: false };
|
|
150
|
+
const kept = [];
|
|
151
|
+
let used = 0;
|
|
152
|
+
for (const line of text.split('\n')) {
|
|
153
|
+
if (used + line.length + 1 > SHOWN_CHARS)
|
|
154
|
+
break;
|
|
155
|
+
kept.push(line);
|
|
156
|
+
used += line.length + 1;
|
|
157
|
+
}
|
|
158
|
+
// One minified line longer than the whole budget: cut it by characters, there is no
|
|
159
|
+
// boundary to respect.
|
|
160
|
+
if (kept.length === 0)
|
|
161
|
+
return { text: text.slice(0, SHOWN_CHARS), cut: true };
|
|
162
|
+
return { text: kept.join('\n'), cut: true };
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Response headers worth a line each: rate limits, the type, and where a redirect points.
|
|
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
|
+
*/
|
|
171
|
+
function shownHeaders(headers) {
|
|
172
|
+
const wanted = (name) => name === 'content-type' || name === 'retry-after' || name === 'location' || name.startsWith('x-ratelimit');
|
|
173
|
+
return Object.entries(headers)
|
|
174
|
+
.filter(([name]) => wanted(name.toLowerCase()))
|
|
175
|
+
.map(([name, value]) => `${name}: ${value}`);
|
|
176
|
+
}
|
|
177
|
+
function allHeaders(headers) {
|
|
178
|
+
return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
179
|
+
}
|
|
180
|
+
/** Headers typed as JSON. Values may carry {NAME} placeholders, the platform fills those in. */
|
|
181
|
+
function headerArg(args) {
|
|
182
|
+
const raw = (0, args_1.flagString)(args, 'header');
|
|
183
|
+
if (raw === undefined)
|
|
184
|
+
return undefined;
|
|
185
|
+
const problem = (0, json_arg_1.jsonProblem)(raw);
|
|
186
|
+
if (problem)
|
|
187
|
+
throw new errors_1.CliError(`--header is not valid JSON: ${raw}`, (0, json_arg_1.jsonHint)('header', raw));
|
|
188
|
+
const parsed = JSON.parse(raw);
|
|
189
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
190
|
+
throw new errors_1.CliError('--header takes an object', 'For example: --header \'{"Client-Id":"{CLIENT_ID}"}\'');
|
|
191
|
+
}
|
|
192
|
+
const headers = {};
|
|
193
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
194
|
+
if (typeof value !== 'string') {
|
|
195
|
+
throw new errors_1.CliError(`The value of ${name} has to be a string`, 'Header values are text');
|
|
196
|
+
}
|
|
197
|
+
headers[name] = value;
|
|
198
|
+
}
|
|
199
|
+
return headers;
|
|
200
|
+
}
|
|
201
|
+
/** Is the body meant to be JSON? A form content-type says it is not, and then it goes as typed. */
|
|
202
|
+
function looksJson(headers) {
|
|
203
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
204
|
+
if (name.toLowerCase() === 'content-type')
|
|
205
|
+
return /json/i.test(value);
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* One-off call to the API behind a connected account, with the platform attaching the
|
|
211
|
+
* credentials. For looking at what an API returns without deploying a function for it.
|
|
212
|
+
*/
|
|
213
|
+
async function connectionsCall(args) {
|
|
214
|
+
const connection = args.words[1];
|
|
215
|
+
const path = args.words[2];
|
|
216
|
+
if (!connection) {
|
|
217
|
+
throw new errors_1.CliError('A connection is required', 'Its alias in this project, or its name from the first column of xflow connections');
|
|
218
|
+
}
|
|
219
|
+
if (!path) {
|
|
220
|
+
throw new errors_1.CliError('A path is required', 'For example: xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01');
|
|
221
|
+
}
|
|
222
|
+
const headers = headerArg(args);
|
|
223
|
+
const body = (0, json_arg_1.bodyArg)(args, 'data');
|
|
224
|
+
// Judged before anything goes over the network, the same as in functions invoke: a broken
|
|
225
|
+
// 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
|
+
const problem = (0, json_arg_1.jsonProblem)(body.text);
|
|
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
|
+
}
|
|
234
|
+
const { projectId, client } = await (0, session_1.projectTarget)(args);
|
|
235
|
+
const data = await (0, api_1.apiJson)(client, `/api/v1/projects/${projectId}/connections/call`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
body: {
|
|
238
|
+
connection,
|
|
239
|
+
path,
|
|
240
|
+
method: (0, args_1.flagString)(args, 'method') ?? (body ? 'POST' : 'GET'),
|
|
241
|
+
...(body ? { body: body.text } : {}),
|
|
242
|
+
...(headers ? { headers } : {}),
|
|
243
|
+
},
|
|
244
|
+
// Past the platform's own cap on the call, so a slow API reads as a slow API and not as
|
|
245
|
+
// an unreachable platform.
|
|
246
|
+
timeoutMs: 60_000,
|
|
247
|
+
});
|
|
248
|
+
if ((0, args_1.flagBool)(args, 'json')) {
|
|
249
|
+
(0, ui_1.out)(JSON.stringify(data, null, 2));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const { request, response } = data;
|
|
253
|
+
// Named the account only when it was not called by its alias: with several accounts of one
|
|
254
|
+
// service under the same name, the alias is the only thing the caller already knew.
|
|
255
|
+
if (!data.alias)
|
|
256
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${data.label} (${data.connection_id}), not linked to this project`));
|
|
257
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${request.method} ${request.url}`));
|
|
258
|
+
for (const line of allHeaders(request.headers))
|
|
259
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${line}`));
|
|
260
|
+
const size = response.truncated ? `at least ${(0, ui_1.formatBytes)(response.bytes)}` : (0, ui_1.formatBytes)(response.bytes);
|
|
261
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${response.status} in ${data.elapsed_ms} ms, ${size}`));
|
|
262
|
+
for (const line of shownHeaders(response.headers))
|
|
263
|
+
(0, ui_1.note)((0, ui_1.dim)(` ${line}`));
|
|
264
|
+
const text = typeof response.body === 'string' ? response.body : JSON.stringify(response.body, null, 2);
|
|
265
|
+
const shown = head(text);
|
|
266
|
+
if (shown.text)
|
|
267
|
+
(0, ui_1.out)(shown.text);
|
|
268
|
+
if (shown.cut || response.truncated) {
|
|
269
|
+
(0, ui_1.note)((0, ui_1.dim)(' This is the head of the answer, not all of it. Narrow the request with the API own ' +
|
|
270
|
+
'parameters (a limit, a shorter period), or process the whole set in a cloud function'));
|
|
271
|
+
}
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
throw new errors_1.CliError(`The API answered ${response.status}`, 'The answer above is theirs, not the platform own');
|
|
274
|
+
}
|
|
275
|
+
}
|
package/dist/flags.js
CHANGED
|
@@ -61,6 +61,7 @@ exports.KNOWN = {
|
|
|
61
61
|
'connections list': ['project'],
|
|
62
62
|
'connections link': ['as', 'project'],
|
|
63
63
|
'connections unlink': ['force'],
|
|
64
|
+
'connections call': ['method', 'data', 'data-file', 'header', 'json', 'project'],
|
|
64
65
|
schedules: ['project'],
|
|
65
66
|
'schedules list': ['project'],
|
|
66
67
|
'schedules set': ['payload', 'payload-file', 'project'],
|
package/dist/help.js
CHANGED
|
@@ -367,6 +367,8 @@ the platform into the function.
|
|
|
367
367
|
xflow connections link <name> --as ALIAS
|
|
368
368
|
hand its credentials to the functions
|
|
369
369
|
xflow connections unlink <ALIAS> take them away again
|
|
370
|
+
xflow connections call <name> <path>
|
|
371
|
+
ask that API something, once, without deploying
|
|
370
372
|
|
|
371
373
|
Every row says whether the connection is linked to this project (its alias) and what
|
|
372
374
|
state the access is in. ${(0, ui_1.bold)('available, not linked')} is the useful one: the account
|
|
@@ -394,7 +396,46 @@ not to look for another route. Either way only the accounts granted to you perso
|
|
|
394
396
|
be linked at all: that rule holds whatever the key is allowed to do.
|
|
395
397
|
|
|
396
398
|
Connecting a new account and switching one off stay with a person, in the platform
|
|
397
|
-
interface. There is no command for either
|
|
399
|
+
interface. There is no command for either.
|
|
400
|
+
|
|
401
|
+
${(0, ui_1.bold)('call')} answers the question an API only answers by being asked: what does this
|
|
402
|
+
endpoint actually return, and is the field you need in it. The platform attaches the
|
|
403
|
+
credentials of the connected account and makes the request itself, so nothing has to be
|
|
404
|
+
deployed to find out, and the key never reaches you.
|
|
405
|
+
|
|
406
|
+
xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
|
|
407
|
+
xflow connections call AMO /api/v4/leads --method POST --data '{"name":"Test"}'
|
|
408
|
+
xflow connections call OZON /v1/product/list --method POST --data '{}' \\
|
|
409
|
+
--header '{"Client-Id":"{CLIENT_ID}"}'
|
|
410
|
+
|
|
411
|
+
The path is written after the address of the connector, query string and all, or given in
|
|
412
|
+
full as ${(0, ui_1.bold)('https://...')}. Only the hosts of that connector are reachable, and a
|
|
413
|
+
refusal lists them. ${(0, ui_1.bold)('--method')} defaults to GET, or to POST when there is a body.
|
|
414
|
+
|
|
415
|
+
${(0, ui_1.bold)('{NAME}')} in the path or in a header value is filled in by the platform from the
|
|
416
|
+
fields of the connection. That is how the two-header APIs are called: Ozon wants
|
|
417
|
+
${(0, ui_1.bold)('Client-Id')} beside its key, Yandex Direct wants ${(0, ui_1.bold)('Client-Login')} for an
|
|
418
|
+
agency account, and you never see those values. The names are the ones ${(0, ui_1.bold)('xflow env')}
|
|
419
|
+
shows without the alias in front: ${(0, ui_1.bold)('OZON_CLIENT_ID')} is written ${(0, ui_1.bold)('{CLIENT_ID}')}.
|
|
420
|
+
A name the connection does not have is left in the text as you typed it, so a typo shows up
|
|
421
|
+
in the printed request instead of failing somewhere far away.
|
|
422
|
+
|
|
423
|
+
The answer is printed head first, at most a few kilobytes, and it says so when there is
|
|
424
|
+
more. Ask for the shape, not the volume: one day and ${(0, ui_1.bold)('limit=1')} tells you what the
|
|
425
|
+
fields are called, while the whole dataset belongs in a cloud function. ${(0, ui_1.bold)('--json')}
|
|
426
|
+
prints the envelope for a pipe: ${(0, ui_1.bold)("--json | jq '.response.body | keys'")} gives the
|
|
427
|
+
field names without the body ever entering your context.
|
|
428
|
+
|
|
429
|
+
An account linked to this project is called by its alias. One that is only available is
|
|
430
|
+
called by its name, and no linking is needed: what the command asks is that you could have
|
|
431
|
+
linked it yourself, which needs the ${(0, ui_1.bold)('connections:link')} right and the account being
|
|
432
|
+
granted to you personally. Either way nothing widens: linked credentials already reach the
|
|
433
|
+
functions of this project, and an account you may link you may also read by deploying a
|
|
434
|
+
function into it. An account granted to somebody else stays out of reach, and the refusal
|
|
435
|
+
says which of the two is missing, the right or the grant.
|
|
436
|
+
|
|
437
|
+
The call goes out for real: ${(0, ui_1.bold)('POST')} and ${(0, ui_1.bold)('DELETE')} change things in
|
|
438
|
+
somebody's live account, and no flag will undo that.`,
|
|
398
439
|
schedules: `${(0, ui_1.bold)('xflow schedules')}: running functions on a timer
|
|
399
440
|
|
|
400
441
|
A schedule is a Yandex timer trigger: it calls the function itself, with no
|
package/dist/json-arg.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.jsonProblem = jsonProblem;
|
|
4
4
|
exports.jsonHint = jsonHint;
|
|
5
|
+
exports.bodyArg = bodyArg;
|
|
5
6
|
exports.jsonArg = jsonArg;
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
7
8
|
const args_1 = require("./args");
|
|
@@ -39,8 +40,13 @@ function jsonHint(flag, raw) {
|
|
|
39
40
|
}
|
|
40
41
|
return `A single JSON value is expected, or keep the body in a file: ${file}`;
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The body from --<flag> or --<flag>-file, whatever it is.
|
|
45
|
+
*
|
|
46
|
+
* Not every body is JSON: a connector call carrying a form body sends the text as typed, and
|
|
47
|
+
* judging it as JSON there would refuse a correct request.
|
|
48
|
+
*/
|
|
49
|
+
function bodyArg(args, flag) {
|
|
44
50
|
const fileFlag = `${flag}-file`;
|
|
45
51
|
const inline = args.flags[flag];
|
|
46
52
|
const path = (0, args_1.flagString)(args, fileFlag);
|
|
@@ -49,21 +55,25 @@ function jsonArg(args, flag) {
|
|
|
49
55
|
}
|
|
50
56
|
if (path !== undefined) {
|
|
51
57
|
if (!(0, node_fs_1.existsSync)(path)) {
|
|
52
|
-
throw new errors_1.CliError(`No file ${path}`, `--${fileFlag} takes the path to a file with
|
|
58
|
+
throw new errors_1.CliError(`No file ${path}`, `--${fileFlag} takes the path to a file with the body`);
|
|
53
59
|
}
|
|
54
|
-
|
|
55
|
-
const problem = jsonProblem(text);
|
|
56
|
-
if (problem)
|
|
57
|
-
throw new errors_1.CliError(`${path} is not valid JSON: ${problem}`, 'The file has to hold one JSON value');
|
|
58
|
-
return text;
|
|
60
|
+
return { text: (0, node_fs_1.readFileSync)(path, 'utf-8').trim(), fromFile: path };
|
|
59
61
|
}
|
|
60
62
|
if (inline === undefined)
|
|
61
63
|
return undefined;
|
|
62
64
|
if (inline === true)
|
|
63
65
|
throw new errors_1.CliError(`--${flag} needs a value`, `For example: --${flag} '{"mode":"full"}'`);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
return { text: String(inline).trim(), fromFile: null };
|
|
67
|
+
}
|
|
68
|
+
/** The body from --<flag> or --<flag>-file, already known to be JSON. */
|
|
69
|
+
function jsonArg(args, flag) {
|
|
70
|
+
const body = bodyArg(args, flag);
|
|
71
|
+
if (body === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const problem = jsonProblem(body.text);
|
|
74
|
+
if (!problem)
|
|
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));
|
|
69
79
|
}
|
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.14.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
|
@@ -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.
|
|
27
|
+
These instructions ship with xflow CLI 0.14.0. 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
|
|
|
@@ -364,10 +364,46 @@ naming rules and the flags. Unlink refuses while a function still reads one of t
|
|
|
364
364
|
variables and names those functions, so read that list before reaching for `--force`.
|
|
365
365
|
|
|
366
366
|
Linking needs the `connections:link` right on the key, and only accounts granted to the
|
|
367
|
-
owner of the key personally can be linked at all
|
|
367
|
+
owner of the key personally can be linked at all; the same right covers calling an unlinked
|
|
368
|
+
account. If the right was taken away, say so and
|
|
368
369
|
ask the person to turn it back on in the platform settings under Developers: a key cannot
|
|
369
370
|
grant it to itself. Connecting a new account and switching one off stay with a person too.
|
|
370
371
|
|
|
372
|
+
### Asking an API what it returns
|
|
373
|
+
|
|
374
|
+
Before writing a function against an unfamiliar API, find out what it actually answers:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
xflow connections call WB /api/v1/supplier/stocks?dateFrom=2026-09-01
|
|
378
|
+
xflow connections call "Яндекс Метрика" /management/v1/counters
|
|
379
|
+
xflow connections call OZON /v1/product/list --method POST --data '{}' \
|
|
380
|
+
--header '{"Client-Id":"{CLIENT_ID}"}'
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
The platform attaches the credentials and makes the request itself, so no deploy is needed and
|
|
384
|
+
the key never reaches you. A linked account is called by its alias, an `available, not linked`
|
|
385
|
+
one by its name: **linking first is not needed to look**, so do not ask a person about that.
|
|
386
|
+
|
|
387
|
+
**Calling is looking, not wiring**: no variable reaches the functions because you called
|
|
388
|
+
something. To use the account in code, link it and deploy.
|
|
389
|
+
|
|
390
|
+
Two refusals do need a person, and different ones: `connections:link` right — the owner of the
|
|
391
|
+
key grants it under Developers; `not granted to you` — ask whoever connected the account.
|
|
392
|
+
|
|
393
|
+
`{NAME}` in the path or in a header value is filled in from the fields of the connection: the
|
|
394
|
+
names are what `xflow env` shows without the alias in front, so `OZON_CLIENT_ID` is written
|
|
395
|
+
`{CLIENT_ID}`. That is how APIs wanting a second header are called, and you never see the
|
|
396
|
+
value. Only the hosts of that connector are reachable and a refusal lists them.
|
|
397
|
+
|
|
398
|
+
**Ask for the shape, not the volume.** One day and `limit=1` is enough to learn what the
|
|
399
|
+
fields are called; the answer is printed head first and says when there is more. The whole
|
|
400
|
+
dataset belongs in a cloud function, which is where you were going anyway. `--json | jq
|
|
401
|
+
'.response.body | keys'` gives the field names without the body entering your context at all.
|
|
402
|
+
|
|
403
|
+
The call goes out for real, against a live account: `POST` and `DELETE` change things there,
|
|
404
|
+
and nothing undoes that. Read the method of the endpoint before calling it, and ask the
|
|
405
|
+
person first when the endpoint is not clearly a read.
|
|
406
|
+
|
|
371
407
|
## Schedules
|
|
372
408
|
|
|
373
409
|
`xflow schedules set report "0 3 ? * * *"` runs a function daily at 03:00. Six fields, UTC,
|