@xano/cli 1.0.6-beta.14 → 1.0.6-beta.16

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 CHANGED
@@ -323,6 +323,7 @@ xano function run <name> --data email=jo@x.com --data age:=30 --data active:=tru
323
323
  xano function run <name> --json @payload.json --data env=staging # base payload + override
324
324
  echo '{"email":"jo@x.com"}' | xano function run <name> --stdin -o json | jq .result
325
325
  xano function run <name> --branch dev --logs # run on a branch, show execution logs
326
+ xano function run <name> --datasource test # run against the 'test' data source
326
327
  ```
327
328
 
328
329
  Input flexibility for `function run` (assembled into one JSON `input` object):
@@ -335,6 +336,11 @@ Input flexibility for `function run` (assembled into one JSON `input` object):
335
336
  | `--json '<inline>'` / `--json @file.json` / `--json -` | a base JSON object (stdin with `-`) |
336
337
  | `--stdin` | read the JSON object from stdin (same as `--json -`) |
337
338
 
339
+ `--datasource <label>` runs the function against a non-live data source by sending the
340
+ `X-Data-Source` header, so table reads and writes hit that data source's tables. Omit it
341
+ to run against `live`; an unknown label is rejected by the server with
342
+ `Invalid data source.`
343
+
338
344
  Merge order is JSON base first, then `--data` overrides. Missing required inputs are
339
345
  prompted for on an interactive terminal; in a non-TTY (CI) context the command fails
340
346
  listing them. Output defaults to the raw `result` (`-o json`); the exit code is non-zero
@@ -15,6 +15,11 @@ export interface CredentialsFile {
15
15
  };
16
16
  }
17
17
  export declare function buildUserAgent(version: string): string;
18
+ /**
19
+ * True when `error` is the "foreign dispatcher rejected by built-in fetch"
20
+ * failure (Node 26+). The real code lives on `error.cause.code`.
21
+ */
22
+ export declare function isInvalidDispatcherError(error: unknown): boolean;
18
23
  export interface SandboxTenant {
19
24
  created_at?: string;
20
25
  description?: string;
@@ -29,13 +29,46 @@ function resolveRequestTimeoutMs(env = process.env) {
29
29
  return DEFAULT_REQUEST_TIMEOUT_MS;
30
30
  return parsed;
31
31
  }
32
+ /**
33
+ * Whether the built-in `fetch` on this runtime accepts a dispatcher built from
34
+ * our bundled userland `undici`. Node <= 24 accepts it; Node 26 rejects a
35
+ * foreign dispatcher with UND_ERR_INVALID_ARG, which `fetch` surfaces only as
36
+ * the generic "fetch failed" — breaking *every* command (DEV-7773).
37
+ *
38
+ * Starts optimistic (`true`); flipped to `false` the first time a request fails
39
+ * with UND_ERR_INVALID_ARG (see `verboseFetch`, which then retries without the
40
+ * dispatcher). Once flipped, every later request skips the dispatcher outright.
41
+ */
42
+ let dispatcherSupported = true;
43
+ /**
44
+ * True when `error` is the "foreign dispatcher rejected by built-in fetch"
45
+ * failure (Node 26+). The real code lives on `error.cause.code`.
46
+ */
47
+ export function isInvalidDispatcherError(error) {
48
+ if (!(error instanceof Error))
49
+ return false;
50
+ const { cause } = error;
51
+ const code = cause && typeof cause === 'object' && 'code' in cause
52
+ ? cause.code
53
+ : error.code;
54
+ return code === 'UND_ERR_INVALID_ARG';
55
+ }
32
56
  /**
33
57
  * Lazily-built undici dispatcher whose header/body inactivity timeouts match our
34
58
  * request timeout, so undici's internal 300s default never fires first. Built
35
59
  * once and reused across requests. `timeout === 0` means "no bound".
60
+ *
61
+ * Returns `undefined` when this runtime has been found to reject a foreign
62
+ * dispatcher (Node 26+, DEV-7773). Dropping the dispatcher does NOT regress the
63
+ * large-push timeout: the per-request `AbortSignal.timeout(timeoutMs)` in
64
+ * `verboseFetch` already bounds the whole request (headers included) to the same
65
+ * 15-min ceiling the dispatcher enforced, so undici's hidden 300s
66
+ * `headersTimeout` is subsumed by it.
36
67
  */
37
68
  let sharedDispatcher;
38
69
  function getRequestDispatcher(timeoutMs) {
70
+ if (!dispatcherSupported)
71
+ return undefined;
39
72
  if (timeoutMs === 0) {
40
73
  sharedDispatcher ??= new Agent({ bodyTimeout: 0, headersTimeout: 0 });
41
74
  return sharedDispatcher;
@@ -324,15 +357,20 @@ export default class BaseCommand extends Command {
324
357
  ...options.headers,
325
358
  };
326
359
  const timeoutMs = resolveRequestTimeoutMs();
327
- const fetchOptions = {
360
+ // Build the fetch options fresh each attempt: a retry needs a new
361
+ // AbortSignal (the first attempt's may already be aborted/consumed) and,
362
+ // when the dispatcher is the culprit, no dispatcher at all.
363
+ const buildFetchOptions = (withDispatcher) => ({
328
364
  ...options,
329
365
  // undici dispatcher: lifts the hidden 300s headers/body timeout to our bound
330
- dispatcher: getRequestDispatcher(timeoutMs),
366
+ ...(withDispatcher ? { dispatcher: getRequestDispatcher(timeoutMs) } : {}),
331
367
  headers,
332
368
  // belt-and-suspenders hard ceiling that also covers DNS/connect stalls;
333
369
  // surfaces as a clean AbortError (see describeNetworkError). 0 = no bound.
370
+ // This alone bounds the whole request, so dropping the dispatcher (Node 26)
371
+ // does not lose the large-push timeout protection.
334
372
  ...(timeoutMs > 0 && !options.signal ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
335
- };
373
+ });
336
374
  const contentType = headers['Content-Type'] || 'application/json';
337
375
  if (verbose) {
338
376
  this.log('');
@@ -349,7 +387,26 @@ export default class BaseCommand extends Command {
349
387
  }
350
388
  }
351
389
  const startTime = Date.now();
352
- const response = await fetch(url, fetchOptions);
390
+ let response;
391
+ try {
392
+ response = await fetch(url, buildFetchOptions(dispatcherSupported));
393
+ }
394
+ catch (error) {
395
+ // Node 26+ rejects our userland-undici dispatcher with UND_ERR_INVALID_ARG,
396
+ // surfaced only as "fetch failed" (DEV-7773). Disable it for the rest of the
397
+ // process and retry once without it; the AbortSignal.timeout ceiling above
398
+ // still bounds the request, so the large-push protection is preserved.
399
+ if (dispatcherSupported && isInvalidDispatcherError(error)) {
400
+ dispatcherSupported = false;
401
+ if (verbose) {
402
+ this.log(' (this Node runtime rejected undici dispatcher; retrying without it — see DEV-7773)');
403
+ }
404
+ response = await fetch(url, buildFetchOptions(false));
405
+ }
406
+ else {
407
+ throw error;
408
+ }
409
+ }
353
410
  const elapsed = Date.now() - startTime;
354
411
  if (verbose) {
355
412
  this.log(`← ${response.status} ${response.statusText} (${elapsed}ms)`);
@@ -8,6 +8,7 @@ export default class FunctionRun extends BaseCommand {
8
8
  static flags: {
9
9
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
10
  data: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ datasource: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  json: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  logs: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -17,6 +17,8 @@ export default class FunctionRun extends BaseCommand {
17
17
  `$ xano function:run calcScore --json @payload.json --data env=staging`,
18
18
  `$ echo '{"email":"jo@x.com"}' | xano function:run calcScore --stdin -o json | jq .result`,
19
19
  `$ xano function:run calcScore --branch dev --logs`,
20
+ `$ xano function:run calcScore --datasource test
21
+ # Runs against the 'test' data source instead of 'live'`,
20
22
  ];
21
23
  static flags = {
22
24
  ...BaseCommand.baseFlags,
@@ -30,6 +32,10 @@ export default class FunctionRun extends BaseCommand {
30
32
  multiple: true,
31
33
  required: false,
32
34
  }),
35
+ datasource: Flags.string({
36
+ description: 'Data source to run against, e.g. test (defaults to live)',
37
+ required: false,
38
+ }),
33
39
  json: Flags.string({
34
40
  description: "Input as a JSON object: inline, @file.json, or '-' for stdin",
35
41
  exclusive: ['stdin'],
@@ -106,13 +112,21 @@ export default class FunctionRun extends BaseCommand {
106
112
  const body = { input, name: functionName };
107
113
  if (branch)
108
114
  body.branch = branch;
115
+ const headers = {
116
+ 'accept': 'application/json',
117
+ 'Authorization': `Bearer ${profile.access_token}`,
118
+ 'Content-Type': 'application/json',
119
+ };
120
+ // The Metadata API resolves the workspace's tables against the data source
121
+ // named by X-Data-Source (defaulting to `live`), so the run reads and writes
122
+ // the same tables a request carrying that header would. An unknown label is
123
+ // rejected server-side with "Invalid data source."
124
+ const dataSource = flags.datasource?.trim();
125
+ if (dataSource)
126
+ headers['X-Data-Source'] = dataSource;
109
127
  const response = await this.verboseFetch(apiUrl, {
110
128
  body: JSON.stringify(body),
111
- headers: {
112
- 'accept': 'application/json',
113
- 'Authorization': `Bearer ${profile.access_token}`,
114
- 'Content-Type': 'application/json',
115
- },
129
+ headers,
116
130
  method: 'POST',
117
131
  }, flags.verbose, profile.access_token);
118
132
  if (!response.ok) {
@@ -904,6 +904,7 @@ export function describeNetworkError(error, url, elapsedMs) {
904
904
  ETIMEDOUT: `Connection to ${host} timed out. Check your network or VPN, then retry.`,
905
905
  UND_ERR_CONNECT_TIMEOUT: `Connection to ${host} timed out. Check your network or VPN, then retry.`,
906
906
  UND_ERR_HEADERS_TIMEOUT: `${host} accepted the connection but did not respond in time. The push may be too large; try splitting it or retrying.`,
907
+ UND_ERR_INVALID_ARG: `Your Node runtime rejected the CLI's HTTP dispatcher. Update the CLI, or run it under Node 24 (see DEV-7773).`,
907
908
  };
908
909
  let base;
909
910
  if (code && hints[code]) {