@xano/cli 1.0.4-beta.7 → 1.0.4-beta.9
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 +28 -0
- package/dist/commands/function/run/index.d.ts +40 -0
- package/dist/commands/function/run/index.js +244 -0
- package/dist/commands/tenant/unit_test/run/index.d.ts +1 -0
- package/dist/commands/tenant/unit_test/run/index.js +7 -0
- package/dist/commands/tenant/unit_test/run_all/index.js +7 -3
- package/dist/commands/unit_test/run/index.d.ts +1 -0
- package/dist/commands/unit_test/run/index.js +8 -0
- package/dist/commands/unit_test/run_all/index.js +7 -3
- package/dist/utils/function-input.d.ts +77 -0
- package/dist/utils/function-input.js +148 -0
- package/oclif.manifest.json +3039 -2870
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,8 +255,35 @@ xano function edit <function_id> -f new.xs # Update from file
|
|
|
255
255
|
xano function edit <function_id> -f new.xs --edit # Open in $EDITOR before updating
|
|
256
256
|
cat function.xs | xano function edit <function_id> --stdin # Update from stdin
|
|
257
257
|
xano function edit <function_id> --no-publish # Edit without publishing
|
|
258
|
+
|
|
259
|
+
# Run (execute) a function by name
|
|
260
|
+
xano function run <name> # Prompts for declared inputs (on a TTY)
|
|
261
|
+
xano function run <name> --data email=jo@x.com --data age:=30 --data active:=true
|
|
262
|
+
xano function run <name> --json @payload.json --data env=staging # base payload + override
|
|
263
|
+
echo '{"email":"jo@x.com"}' | xano function run <name> --stdin -o json | jq .result
|
|
264
|
+
xano function run <name> --branch dev --logs # run on a branch, show execution logs
|
|
258
265
|
```
|
|
259
266
|
|
|
267
|
+
Input flexibility for `function run` (assembled into one JSON `input` object):
|
|
268
|
+
|
|
269
|
+
| Form | Meaning |
|
|
270
|
+
| --- | --- |
|
|
271
|
+
| `--data key=value` | string field |
|
|
272
|
+
| `--data key:=<json>` | raw JSON field (`age:=30`, `active:=true`, `tags:='["a","b"]'`) |
|
|
273
|
+
| `--data key@file` | field value read from a file |
|
|
274
|
+
| `--json '<inline>'` / `--json @file.json` / `--json -` | a base JSON object (stdin with `-`) |
|
|
275
|
+
| `--stdin` | read the JSON object from stdin (same as `--json -`) |
|
|
276
|
+
|
|
277
|
+
Merge order is JSON base first, then `--data` overrides. Missing required inputs are
|
|
278
|
+
prompted for on an interactive terminal; in a non-TTY (CI) context the command fails
|
|
279
|
+
listing them. Output defaults to the raw `result` (`-o json`); the exit code is non-zero
|
|
280
|
+
when the function returns an error status.
|
|
281
|
+
|
|
282
|
+
> **Permissions:** running a function requires **Function** (read) plus the **run/debug**
|
|
283
|
+
> action on your workspace role. If your access token's role lacks run/debug, the call is
|
|
284
|
+
> denied with an access error — grant run/debug to the role (or use a token whose role has
|
|
285
|
+
> it). `function list`/`get`/`create`/`edit` only need the Function permission.
|
|
286
|
+
|
|
260
287
|
### Releases
|
|
261
288
|
|
|
262
289
|
All release commands use **release names** (e.g., `v1.0`), not IDs.
|
|
@@ -326,6 +353,7 @@ xano unit_test list --branch dev --obj-type function
|
|
|
326
353
|
|
|
327
354
|
# Run a single unit test
|
|
328
355
|
xano unit_test run <unit_test_id>
|
|
356
|
+
xano unit_test run <unit_test_id> --branch dev # run the test from a specific branch
|
|
329
357
|
|
|
330
358
|
# Run all unit tests
|
|
331
359
|
xano unit_test run_all
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import BaseCommand from '../../../base-command.js';
|
|
2
|
+
export default class FunctionRun extends BaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
data: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
json: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
logs: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
+
'no-input-check': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
stdin: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
+
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
+
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
+
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
+
};
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
private emitResult;
|
|
24
|
+
/**
|
|
25
|
+
* Fetch a single function record by name via the list endpoint's search,
|
|
26
|
+
* returning the exact-name match (so `calc` doesn't accidentally match
|
|
27
|
+
* `calcScore`). Best-effort: returns undefined and warns on any failure so a
|
|
28
|
+
* transient introspection problem never blocks an otherwise-valid run.
|
|
29
|
+
*/
|
|
30
|
+
private fetchFunctionByName;
|
|
31
|
+
/**
|
|
32
|
+
* Prompt for each missing required input on an interactive TTY. Values are
|
|
33
|
+
* read as raw JSON when they parse as such (so numbers/bools/objects work),
|
|
34
|
+
* otherwise treated as a string — mirroring the `key:=`/`key=` distinction.
|
|
35
|
+
* Non-TTY: fail fast with the list of missing params (never hang on a prompt).
|
|
36
|
+
*/
|
|
37
|
+
private promptForMissing;
|
|
38
|
+
private readStdinSync;
|
|
39
|
+
private resolveWorkspaceId;
|
|
40
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import BaseCommand from '../../../base-command.js';
|
|
4
|
+
import { assembleInput, loadJsonSource, parseDataPairs, validateAgainstSchema, } from '../../../utils/function-input.js';
|
|
5
|
+
export default class FunctionRun extends BaseCommand {
|
|
6
|
+
static args = {
|
|
7
|
+
name: Args.string({
|
|
8
|
+
description: 'Name of the function to run (interactive if omitted with --name unset)',
|
|
9
|
+
required: false,
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
static description = 'Run (execute) a named function in a workspace and print its result';
|
|
13
|
+
static examples = [
|
|
14
|
+
`$ xano function:run calcScore -w 40
|
|
15
|
+
# Prompts for any declared inputs, then runs the function`,
|
|
16
|
+
`$ xano function:run calcScore --data email=jo@x.com --data age:=30 --data active:=true`,
|
|
17
|
+
`$ xano function:run calcScore --json @payload.json --data env=staging`,
|
|
18
|
+
`$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
|
|
19
|
+
`$ xano function:run calcScore --branch dev --logs`,
|
|
20
|
+
];
|
|
21
|
+
static flags = {
|
|
22
|
+
...BaseCommand.baseFlags,
|
|
23
|
+
branch: Flags.string({
|
|
24
|
+
description: 'Branch to run from (defaults to profile branch, then main)',
|
|
25
|
+
required: false,
|
|
26
|
+
}),
|
|
27
|
+
data: Flags.string({
|
|
28
|
+
char: 'd',
|
|
29
|
+
description: 'Input field as key=value (string), key:=json (raw JSON), or key@file (file contents). Repeatable.',
|
|
30
|
+
multiple: true,
|
|
31
|
+
required: false,
|
|
32
|
+
}),
|
|
33
|
+
json: Flags.string({
|
|
34
|
+
description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
|
|
35
|
+
exclusive: ['stdin'],
|
|
36
|
+
required: false,
|
|
37
|
+
}),
|
|
38
|
+
logs: Flags.boolean({
|
|
39
|
+
default: false,
|
|
40
|
+
description: 'Print the execution logs returned by the debugger',
|
|
41
|
+
required: false,
|
|
42
|
+
}),
|
|
43
|
+
'name': Flags.string({
|
|
44
|
+
char: 'n',
|
|
45
|
+
description: 'Name of the function to run (alternative to the positional argument)',
|
|
46
|
+
required: false,
|
|
47
|
+
}),
|
|
48
|
+
'no-input-check': Flags.boolean({
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Skip local schema validation and interactive prompting; send the payload as-is',
|
|
51
|
+
required: false,
|
|
52
|
+
}),
|
|
53
|
+
output: Flags.string({
|
|
54
|
+
char: 'o',
|
|
55
|
+
default: 'json',
|
|
56
|
+
description: 'Output format',
|
|
57
|
+
options: ['json', 'summary'],
|
|
58
|
+
required: false,
|
|
59
|
+
}),
|
|
60
|
+
stdin: Flags.boolean({
|
|
61
|
+
char: 's',
|
|
62
|
+
default: false,
|
|
63
|
+
description: "Read the input JSON object from stdin (same as --json -)",
|
|
64
|
+
exclusive: ['json'],
|
|
65
|
+
required: false,
|
|
66
|
+
}),
|
|
67
|
+
workspace: Flags.string({
|
|
68
|
+
char: 'w',
|
|
69
|
+
description: 'Workspace ID (optional if set in profile)',
|
|
70
|
+
required: false,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
async run() {
|
|
74
|
+
const { args, flags } = await this.parse(FunctionRun);
|
|
75
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
76
|
+
const functionName = args.name ?? flags.name;
|
|
77
|
+
if (!functionName) {
|
|
78
|
+
this.error('A function name is required. Provide it as an argument or with --name.');
|
|
79
|
+
}
|
|
80
|
+
const workspaceId = this.resolveWorkspaceId(flags, profile, profileName);
|
|
81
|
+
const branch = flags.branch ?? profile.branch ?? '';
|
|
82
|
+
// 1) Assemble the input object from JSON base + --data overrides.
|
|
83
|
+
const jsonSource = flags.stdin ? '-' : flags.json;
|
|
84
|
+
const jsonBase = loadJsonSource(jsonSource, { stdin: () => this.readStdinSync() });
|
|
85
|
+
let input = assembleInput({ dataPairs: parseDataPairs(flags.data ?? []), jsonBase });
|
|
86
|
+
// 2) Introspect the function's declared inputs (best-effort) to validate,
|
|
87
|
+
// warn on obvious mismatches, and prompt for missing required params.
|
|
88
|
+
if (!flags['no-input-check']) {
|
|
89
|
+
const fn = await this.fetchFunctionByName({
|
|
90
|
+
branch,
|
|
91
|
+
name: functionName,
|
|
92
|
+
profile,
|
|
93
|
+
verbose: flags.verbose,
|
|
94
|
+
workspaceId,
|
|
95
|
+
});
|
|
96
|
+
const schema = fn?.input;
|
|
97
|
+
const { missingRequired, warnings } = validateAgainstSchema(input, schema);
|
|
98
|
+
for (const w of warnings)
|
|
99
|
+
this.warn(w);
|
|
100
|
+
if (missingRequired.length > 0) {
|
|
101
|
+
input = await this.promptForMissing(input, missingRequired);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 3) Execute via the meta run endpoint.
|
|
105
|
+
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function/run`;
|
|
106
|
+
const body = { input, name: functionName };
|
|
107
|
+
if (branch)
|
|
108
|
+
body.branch = branch;
|
|
109
|
+
const response = await this.verboseFetch(apiUrl, {
|
|
110
|
+
body: JSON.stringify(body),
|
|
111
|
+
headers: {
|
|
112
|
+
'accept': 'application/json',
|
|
113
|
+
'Authorization': `Bearer ${profile.access_token}`,
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
method: 'POST',
|
|
117
|
+
}, flags.verbose, profile.access_token);
|
|
118
|
+
if (!response.ok) {
|
|
119
|
+
const message = await this.parseApiError(response, 'Failed to run function');
|
|
120
|
+
this.error(message);
|
|
121
|
+
}
|
|
122
|
+
const result = (await response.json());
|
|
123
|
+
this.emitResult(result, flags.output, flags.logs);
|
|
124
|
+
// 4) Compose in scripts/CI: non-zero exit when the function errored.
|
|
125
|
+
if (result.status && result.status !== 'ok') {
|
|
126
|
+
this.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
emitResult(result, output, showLogs) {
|
|
130
|
+
if (output === 'json') {
|
|
131
|
+
// Raw result object so `| jq .result` works predictably.
|
|
132
|
+
this.log(JSON.stringify(result.result ?? null, null, 2));
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const status = result.status ?? 'ok';
|
|
136
|
+
this.log(`Status: ${status}`);
|
|
137
|
+
this.log('Result:');
|
|
138
|
+
this.log(JSON.stringify(result.result ?? null, null, 2));
|
|
139
|
+
}
|
|
140
|
+
if (showLogs && Array.isArray(result.logs) && result.logs.length > 0) {
|
|
141
|
+
this.log('');
|
|
142
|
+
this.log('Logs:');
|
|
143
|
+
for (const entry of result.logs) {
|
|
144
|
+
this.log(typeof entry === 'string' ? entry : JSON.stringify(entry));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Fetch a single function record by name via the list endpoint's search,
|
|
150
|
+
* returning the exact-name match (so `calc` doesn't accidentally match
|
|
151
|
+
* `calcScore`). Best-effort: returns undefined and warns on any failure so a
|
|
152
|
+
* transient introspection problem never blocks an otherwise-valid run.
|
|
153
|
+
*/
|
|
154
|
+
async fetchFunctionByName(opts) {
|
|
155
|
+
const { branch, name, profile, verbose, workspaceId } = opts;
|
|
156
|
+
// eslint-disable-next-line camelcase -- external Metadata API query param
|
|
157
|
+
const params = new URLSearchParams({ per_page: '100', search: name });
|
|
158
|
+
if (branch)
|
|
159
|
+
params.set('branch', branch);
|
|
160
|
+
const url = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/function?${params.toString()}`;
|
|
161
|
+
try {
|
|
162
|
+
const response = await this.verboseFetch(url, { headers: { accept: 'application/json', Authorization: `Bearer ${profile.access_token}` }, method: 'GET' }, verbose, profile.access_token);
|
|
163
|
+
if (!response.ok)
|
|
164
|
+
return undefined;
|
|
165
|
+
const page = (await response.json());
|
|
166
|
+
const items = Array.isArray(page) ? page : (page.items ?? []);
|
|
167
|
+
const exact = items.filter((f) => f.name === name);
|
|
168
|
+
if (exact.length > 1) {
|
|
169
|
+
this.warn(`Multiple functions named '${name}' found; skipping input validation.`);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
return exact[0];
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Introspection is a convenience, not a requirement.
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Prompt for each missing required input on an interactive TTY. Values are
|
|
181
|
+
* read as raw JSON when they parse as such (so numbers/bools/objects work),
|
|
182
|
+
* otherwise treated as a string — mirroring the `key:=`/`key=` distinction.
|
|
183
|
+
* Non-TTY: fail fast with the list of missing params (never hang on a prompt).
|
|
184
|
+
*/
|
|
185
|
+
async promptForMissing(input, missing) {
|
|
186
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
187
|
+
const names = missing.map((p) => `${p.name}${p.type ? ` (${p.type})` : ''}`).join(', ');
|
|
188
|
+
this.error(`Missing required input: ${names}. ` +
|
|
189
|
+
`Provide via --data/--json, or run interactively in a terminal.`);
|
|
190
|
+
}
|
|
191
|
+
const readline = await import('node:readline');
|
|
192
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
193
|
+
const ask = (message) => new Promise((resolve) => {
|
|
194
|
+
rl.question(message, resolve);
|
|
195
|
+
});
|
|
196
|
+
const filled = { ...input };
|
|
197
|
+
try {
|
|
198
|
+
for (const param of missing) {
|
|
199
|
+
const hint = [param.type, param.description].filter(Boolean).join(' — ');
|
|
200
|
+
// eslint-disable-next-line no-await-in-loop
|
|
201
|
+
const raw = await ask(`${param.name}${hint ? ` (${hint})` : ''}: `);
|
|
202
|
+
filled[param.name] = coercePromptValue(raw, param);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
rl.close();
|
|
207
|
+
}
|
|
208
|
+
return filled;
|
|
209
|
+
}
|
|
210
|
+
readStdinSync() {
|
|
211
|
+
// Synchronous read of all of stdin (fd 0); used for --json - / --stdin.
|
|
212
|
+
try {
|
|
213
|
+
return readFileSync(0, 'utf8');
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
this.error(`Failed to read from stdin: ${error instanceof Error ? error.message : String(error)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
resolveWorkspaceId(flags, profile, profileName) {
|
|
220
|
+
if (flags.workspace)
|
|
221
|
+
return flags.workspace;
|
|
222
|
+
if (profile.workspace)
|
|
223
|
+
return profile.workspace;
|
|
224
|
+
this.error(`Workspace ID is required. Either:\n` +
|
|
225
|
+
` 1. Provide it as a flag: xano function:run <name> -w <workspace_id>\n` +
|
|
226
|
+
` 2. Set it in your profile using: xano profile:edit ${profileName} -w <workspace_id>`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Coerce a prompted string into the value the parameter likely expects. Non-text
|
|
231
|
+
* params attempt a JSON parse (so `30`, `true`, `["a"]` become typed values);
|
|
232
|
+
* text params keep the raw string. Falls back to the raw string on parse errors.
|
|
233
|
+
*/
|
|
234
|
+
function coercePromptValue(raw, param) {
|
|
235
|
+
const isTextLike = !param.type || param.type === 'text' || param.type === 'uuid';
|
|
236
|
+
if (isTextLike)
|
|
237
|
+
return raw;
|
|
238
|
+
try {
|
|
239
|
+
return JSON.parse(raw);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return raw;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -6,6 +6,7 @@ export default class TenantUnitTestRun extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
tenant: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -17,6 +17,11 @@ Result: PASS
|
|
|
17
17
|
];
|
|
18
18
|
static flags = {
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
|
+
branch: Flags.string({
|
|
21
|
+
char: 'b',
|
|
22
|
+
description: 'Branch the unit test belongs to (uses profile branch if not provided, then the live branch)',
|
|
23
|
+
required: false,
|
|
24
|
+
}),
|
|
20
25
|
output: Flags.string({
|
|
21
26
|
char: 'o',
|
|
22
27
|
default: 'summary',
|
|
@@ -43,12 +48,14 @@ Result: PASS
|
|
|
43
48
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
44
49
|
}
|
|
45
50
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
51
|
+
const branch = flags.branch ?? profile.branch;
|
|
46
52
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test/${encodeURIComponent(args.unit_test_id)}/run`;
|
|
47
53
|
try {
|
|
48
54
|
if (flags.output === 'summary') {
|
|
49
55
|
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
50
56
|
}
|
|
51
57
|
const response = await this.verboseFetch(apiUrl, {
|
|
58
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
52
59
|
headers: {
|
|
53
60
|
accept: 'application/json',
|
|
54
61
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -18,7 +18,7 @@ Results: 4 passed, 1 failed
|
|
|
18
18
|
...BaseCommand.baseFlags,
|
|
19
19
|
branch: Flags.string({
|
|
20
20
|
char: 'b',
|
|
21
|
-
description: '
|
|
21
|
+
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
22
22
|
required: false,
|
|
23
23
|
}),
|
|
24
24
|
'obj-type': Flags.string({
|
|
@@ -53,11 +53,14 @@ Results: 4 passed, 1 failed
|
|
|
53
53
|
}
|
|
54
54
|
const tenantName = encodeURIComponent(flags.tenant);
|
|
55
55
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test`;
|
|
56
|
+
// Resolve the branch once so the listed tests and the tests we run come
|
|
57
|
+
// from the same branch.
|
|
58
|
+
const branch = flags.branch ?? profile.branch;
|
|
56
59
|
try {
|
|
57
60
|
const listParams = new URLSearchParams();
|
|
58
61
|
listParams.set('per_page', '10000');
|
|
59
|
-
if (
|
|
60
|
-
listParams.set('branch',
|
|
62
|
+
if (branch)
|
|
63
|
+
listParams.set('branch', branch);
|
|
61
64
|
if (flags['obj-type'])
|
|
62
65
|
listParams.set('obj_type', flags['obj-type']);
|
|
63
66
|
const listResponse = await this.verboseFetch(`${baseUrl}?${listParams}`, {
|
|
@@ -94,6 +97,7 @@ Results: 4 passed, 1 failed
|
|
|
94
97
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
95
98
|
try {
|
|
96
99
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
100
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
97
101
|
headers: {
|
|
98
102
|
accept: 'application/json',
|
|
99
103
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -6,6 +6,7 @@ export default class UnitTestRun extends BaseCommand {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
|
+
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
10
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
11
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
12
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -14,9 +14,15 @@ Running unit test abc-123...
|
|
|
14
14
|
Result: PASS
|
|
15
15
|
`,
|
|
16
16
|
`$ xano unit-test run abc-123 -o json`,
|
|
17
|
+
`$ xano unit-test run abc-123 --branch dev`,
|
|
17
18
|
];
|
|
18
19
|
static flags = {
|
|
19
20
|
...BaseCommand.baseFlags,
|
|
21
|
+
branch: Flags.string({
|
|
22
|
+
char: 'b',
|
|
23
|
+
description: 'Branch the unit test belongs to (uses profile branch if not provided, then the live branch)',
|
|
24
|
+
required: false,
|
|
25
|
+
}),
|
|
20
26
|
output: Flags.string({
|
|
21
27
|
char: 'o',
|
|
22
28
|
default: 'summary',
|
|
@@ -37,12 +43,14 @@ Result: PASS
|
|
|
37
43
|
if (!workspaceId) {
|
|
38
44
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
39
45
|
}
|
|
46
|
+
const branch = flags.branch ?? profile.branch;
|
|
40
47
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/unit_test/${args.unit_test_id}/run`;
|
|
41
48
|
try {
|
|
42
49
|
if (flags.output === 'summary') {
|
|
43
50
|
this.log(`Running unit test ${args.unit_test_id}...`);
|
|
44
51
|
}
|
|
45
52
|
const response = await this.verboseFetch(apiUrl, {
|
|
53
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
46
54
|
headers: {
|
|
47
55
|
'accept': 'application/json',
|
|
48
56
|
'Authorization': `Bearer ${profile.access_token}`,
|
|
@@ -19,7 +19,7 @@ Results: 2 passed, 1 failed
|
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
20
|
branch: Flags.string({
|
|
21
21
|
char: 'b',
|
|
22
|
-
description: '
|
|
22
|
+
description: 'Branch to run tests from (uses profile branch if not provided, then the live branch)',
|
|
23
23
|
required: false,
|
|
24
24
|
}),
|
|
25
25
|
'obj-type': Flags.string({
|
|
@@ -48,12 +48,15 @@ Results: 2 passed, 1 failed
|
|
|
48
48
|
this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
|
|
49
49
|
}
|
|
50
50
|
const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/unit_test`;
|
|
51
|
+
// Resolve the branch once so the listed tests and the tests we run come
|
|
52
|
+
// from the same branch.
|
|
53
|
+
const branch = flags.branch ?? profile.branch;
|
|
51
54
|
try {
|
|
52
55
|
// Step 1: List all unit tests
|
|
53
56
|
const listParams = new URLSearchParams();
|
|
54
57
|
listParams.set('per_page', '10000');
|
|
55
|
-
if (
|
|
56
|
-
listParams.set('branch',
|
|
58
|
+
if (branch) {
|
|
59
|
+
listParams.set('branch', branch);
|
|
57
60
|
}
|
|
58
61
|
if (flags['obj-type']) {
|
|
59
62
|
listParams.set('obj_type', flags['obj-type']);
|
|
@@ -93,6 +96,7 @@ Results: 2 passed, 1 failed
|
|
|
93
96
|
const runUrl = `${baseUrl}/${test.id}/run`;
|
|
94
97
|
try {
|
|
95
98
|
const runResponse = await this.verboseFetch(runUrl, {
|
|
99
|
+
body: JSON.stringify(branch ? { branch } : {}),
|
|
96
100
|
headers: {
|
|
97
101
|
accept: 'application/json',
|
|
98
102
|
Authorization: `Bearer ${profile.access_token}`,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input assembly for `xano function run`.
|
|
3
|
+
*
|
|
4
|
+
* A run's `input` is a single JSON object sent to the Metadata API. Users can
|
|
5
|
+
* build that object from several sources, which this module normalizes and
|
|
6
|
+
* merges:
|
|
7
|
+
*
|
|
8
|
+
* --json <inline | @file | -> a base JSON object
|
|
9
|
+
* --data key=value a string field (httpie convention)
|
|
10
|
+
* --data key:=<json> a raw-JSON field (numbers, bools, arrays, ...)
|
|
11
|
+
* --data key@<file> a field whose value is a file's contents (string)
|
|
12
|
+
*
|
|
13
|
+
* Merge order is: JSON base first, then --data pairs override — so the common
|
|
14
|
+
* "reuse a payload file, tweak one field" flow works without editing the file.
|
|
15
|
+
*
|
|
16
|
+
* Everything here is pure and free of oclif/network deps so it is unit-testable
|
|
17
|
+
* in isolation; the command layer handles fetching, prompting and output.
|
|
18
|
+
*/
|
|
19
|
+
export type JsonObject = Record<string, unknown>;
|
|
20
|
+
/** A single declared function input parameter, as returned by the meta API. */
|
|
21
|
+
export interface FunctionInputParam {
|
|
22
|
+
children?: FunctionInputParam[];
|
|
23
|
+
default?: unknown;
|
|
24
|
+
description?: string;
|
|
25
|
+
name: string;
|
|
26
|
+
nullable?: boolean;
|
|
27
|
+
required?: boolean;
|
|
28
|
+
style?: {
|
|
29
|
+
type?: string;
|
|
30
|
+
};
|
|
31
|
+
type?: string;
|
|
32
|
+
values?: unknown[];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Split a `--data` token into its key, operator and value.
|
|
36
|
+
* Recognizes, in priority order: `:=` (raw json), `@` (file), `=` (string).
|
|
37
|
+
* The operator is the FIRST occurrence scanned left-to-right, but `:=` is
|
|
38
|
+
* detected before a bare `=` at the same position so `k:=1` is raw-json, not
|
|
39
|
+
* a string key `k:` set to `=1`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function splitDataToken(token: string): {
|
|
42
|
+
key: string;
|
|
43
|
+
op: ':=' | '=' | '@';
|
|
44
|
+
value: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Parse `--data` tokens into an object, applying httpie-style typing.
|
|
48
|
+
* Later tokens override earlier ones with the same key.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseDataPairs(pairs: string[], readFile?: (p: string) => string): JsonObject;
|
|
51
|
+
/**
|
|
52
|
+
* Load the `--json` source: inline JSON, `@file`, or `-` (stdin, supplied by
|
|
53
|
+
* the caller via `stdin`). Must parse to a plain object; arrays/scalars are
|
|
54
|
+
* rejected because a function's input is always a keyed object.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadJsonSource(source: string | undefined, opts?: {
|
|
57
|
+
readFile?: (p: string) => string;
|
|
58
|
+
stdin?: () => string;
|
|
59
|
+
}): JsonObject | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Merge a JSON base with --data overrides into the final input object.
|
|
62
|
+
* Precedence: base first, then dataPairs override (shallow).
|
|
63
|
+
*/
|
|
64
|
+
export declare function assembleInput(opts: {
|
|
65
|
+
dataPairs?: JsonObject;
|
|
66
|
+
jsonBase?: JsonObject;
|
|
67
|
+
}): JsonObject;
|
|
68
|
+
/**
|
|
69
|
+
* Validate a supplied input object against a function's declared input schema.
|
|
70
|
+
* Non-destructive: returns lists the caller can act on (prompt for missing
|
|
71
|
+
* required params on a TTY, warn on enum mismatches, etc.). The server remains
|
|
72
|
+
* the source of truth, so type mismatches are warnings, not hard failures.
|
|
73
|
+
*/
|
|
74
|
+
export declare function validateAgainstSchema(input: JsonObject, schema: FunctionInputParam[] | undefined): {
|
|
75
|
+
missingRequired: FunctionInputParam[];
|
|
76
|
+
warnings: string[];
|
|
77
|
+
};
|