@xanots/sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/agent-file-refresh-7XCACU34.js +95 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +184 -0
- package/dist/capture-HUV5BNTC.js +32 -0
- package/dist/chunk-26CNBZE2.js +963 -0
- package/dist/chunk-33ND6LKO.js +78 -0
- package/dist/chunk-3DUAGYGP.js +1550 -0
- package/dist/chunk-3IGNIP6R.js +89 -0
- package/dist/chunk-47WDWMBJ.js +14 -0
- package/dist/chunk-4BXJGVZ3.js +318 -0
- package/dist/chunk-4HT3BNZ7.js +675 -0
- package/dist/chunk-4YMD2OOZ.js +23 -0
- package/dist/chunk-5WGEURVI.js +156 -0
- package/dist/chunk-5YTBTF25.js +29 -0
- package/dist/chunk-6DHBYBTO.js +166 -0
- package/dist/chunk-76QBEIGO.js +66 -0
- package/dist/chunk-7DKX2SPN.js +100 -0
- package/dist/chunk-7REDODS2.js +468 -0
- package/dist/chunk-C56BC2FY.js +254 -0
- package/dist/chunk-DGSF2Q5H.js +60 -0
- package/dist/chunk-EMCAWMHC.js +3982 -0
- package/dist/chunk-EZG76F7R.js +159 -0
- package/dist/chunk-FJ6ZS5KB.js +3607 -0
- package/dist/chunk-GNPVYOPB.js +17 -0
- package/dist/chunk-HJPTWBLH.js +38 -0
- package/dist/chunk-IZW6MK3U.js +248 -0
- package/dist/chunk-LH6Z6KFV.js +247 -0
- package/dist/chunk-MZUI3JNX.js +93 -0
- package/dist/chunk-NDZFBZHC.js +167 -0
- package/dist/chunk-NOO35BQ2.js +37 -0
- package/dist/chunk-NUQCEOKA.js +13 -0
- package/dist/chunk-QTNO2WD6.js +56 -0
- package/dist/chunk-RVB7ITKN.js +49 -0
- package/dist/chunk-TRU5OHQ5.js +6904 -0
- package/dist/chunk-VKFQJEMW.js +814 -0
- package/dist/chunk-VTIL47DT.js +125 -0
- package/dist/chunk-WHOJWOSV.js +112 -0
- package/dist/chunk-WKP4D5MO.js +1692 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-XQ22GLYS.js +118 -0
- package/dist/chunk-YDZYMZBE.js +670 -0
- package/dist/chunk-YGPIY3WT.js +1146 -0
- package/dist/chunk-YYRDLFSJ.js +602 -0
- package/dist/chunk-ZUTSMMAG.js +46 -0
- package/dist/cli.d.ts +331 -0
- package/dist/cli.js +33 -0
- package/dist/codegen-command-OOKK6AZK.js +40 -0
- package/dist/codegen.d.ts +114 -0
- package/dist/codegen.js +71 -0
- package/dist/completion-TIPPZN7H.js +298 -0
- package/dist/config-NL33PN4D.js +84 -0
- package/dist/deploy-command-4EQ7XX5G.js +451 -0
- package/dist/env-target-IFJ5QDHC.js +16 -0
- package/dist/ephemeral-command-3X3I4ULC.js +242 -0
- package/dist/index.d.ts +4477 -0
- package/dist/index.js +300 -0
- package/dist/init-command-R6H7ZXW5.js +27 -0
- package/dist/internal.d.ts +74 -0
- package/dist/internal.js +2065 -0
- package/dist/io-AMIKRLPC.js +11 -0
- package/dist/live-diff-Y3AKETHE.js +196 -0
- package/dist/lock-3CVKALKT.js +37 -0
- package/dist/lock-commands-XTR2YKLY.js +232 -0
- package/dist/login-command-ACJF6KWQ.js +167 -0
- package/dist/logout-command-MX3MJS5U.js +68 -0
- package/dist/loop-OM3P2Z6O.js +93 -0
- package/dist/marketplace-command-RATXTKXN.js +379 -0
- package/dist/meta-client-OW5WKWW7.js +170 -0
- package/dist/node.d.ts +59 -0
- package/dist/node.js +442 -0
- package/dist/profile-command-SWJ3SPKR.js +77 -0
- package/dist/release-command-VV5UVZKL.js +427 -0
- package/dist/response-CVAE2kMj.d.ts +1915 -0
- package/dist/routes-manifest-SP3ZXLMR.js +244 -0
- package/dist/runtime-V4C3AC3A.js +13 -0
- package/dist/sandbox-details-command-HJE5SPVG.js +18 -0
- package/dist/sandbox-export-command-QCJY4GMV.js +24 -0
- package/dist/static-host-D6KS7X45.js +138 -0
- package/dist/store-BQ97nKIF.d.ts +7760 -0
- package/dist/test-command-TSPBQHBM.js +383 -0
- package/dist/validate-command-4ERHJSCO.js +122 -0
- package/dist/verify-rollout-FAVQ6EJY.js +40 -0
- package/dist/workspace-K72NP7SX.js +49 -0
- package/dist/workspace-command-OF23DPNI.js +156 -0
- package/dist/workspace-export-AJMGN3CQ.js +10 -0
- package/llms/fields.md +104 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +11 -0
- package/llms/kinds-core.md +68 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +111 -0
- package/llms/legacy.md +43 -0
- package/llms/object-kinds.md +31 -0
- package/llms/statements-calls.md +23 -0
- package/llms/statements-catalog.md +309 -0
- package/llms/statements-data.md +58 -0
- package/llms/statements-runtime.md +48 -0
- package/llms/tests.md +58 -0
- package/llms/triggers.md +22 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +1672 -0
- package/llms.txt +483 -0
- package/manifest.json +9592 -0
- package/package.json +110 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fetchWorkspaceBundle,
|
|
3
|
+
runCodegenCommand
|
|
4
|
+
} from "./chunk-TRU5OHQ5.js";
|
|
5
|
+
import "./chunk-26CNBZE2.js";
|
|
6
|
+
import {
|
|
7
|
+
resolveOutputTarget
|
|
8
|
+
} from "./chunk-XQ22GLYS.js";
|
|
9
|
+
import "./chunk-QTNO2WD6.js";
|
|
10
|
+
import "./chunk-ZUTSMMAG.js";
|
|
11
|
+
import "./chunk-WKP4D5MO.js";
|
|
12
|
+
import "./chunk-VKFQJEMW.js";
|
|
13
|
+
import "./chunk-47WDWMBJ.js";
|
|
14
|
+
import "./chunk-X4DVXBFY.js";
|
|
15
|
+
import "./chunk-7DKX2SPN.js";
|
|
16
|
+
import {
|
|
17
|
+
printMicroserviceSection,
|
|
18
|
+
readMicroservices
|
|
19
|
+
} from "./chunk-NDZFBZHC.js";
|
|
20
|
+
import {
|
|
21
|
+
isMachineOutput,
|
|
22
|
+
writeJson
|
|
23
|
+
} from "./chunk-NUQCEOKA.js";
|
|
24
|
+
import {
|
|
25
|
+
fetchOrExplain,
|
|
26
|
+
httpFailure
|
|
27
|
+
} from "./chunk-3IGNIP6R.js";
|
|
28
|
+
import {
|
|
29
|
+
ENV_WORKSPACE,
|
|
30
|
+
getAccessToken
|
|
31
|
+
} from "./chunk-C56BC2FY.js";
|
|
32
|
+
import "./chunk-7REDODS2.js";
|
|
33
|
+
import "./chunk-MZUI3JNX.js";
|
|
34
|
+
import "./chunk-RVB7ITKN.js";
|
|
35
|
+
import "./chunk-4BXJGVZ3.js";
|
|
36
|
+
import "./chunk-FJ6ZS5KB.js";
|
|
37
|
+
import "./chunk-YGPIY3WT.js";
|
|
38
|
+
import {
|
|
39
|
+
removedSubcommand,
|
|
40
|
+
unknownSubcommand
|
|
41
|
+
} from "./chunk-IZW6MK3U.js";
|
|
42
|
+
import {
|
|
43
|
+
formatFields,
|
|
44
|
+
stdoutStyle,
|
|
45
|
+
step,
|
|
46
|
+
success
|
|
47
|
+
} from "./chunk-EZG76F7R.js";
|
|
48
|
+
import "./chunk-3DUAGYGP.js";
|
|
49
|
+
import "./chunk-YYRDLFSJ.js";
|
|
50
|
+
import "./chunk-EMCAWMHC.js";
|
|
51
|
+
import "./chunk-WHOJWOSV.js";
|
|
52
|
+
import "./chunk-LH6Z6KFV.js";
|
|
53
|
+
import "./chunk-HJPTWBLH.js";
|
|
54
|
+
import "./chunk-GNPVYOPB.js";
|
|
55
|
+
import "./chunk-4HT3BNZ7.js";
|
|
56
|
+
|
|
57
|
+
// src/emit/workspace-command.ts
|
|
58
|
+
import { writeFileSync } from "fs";
|
|
59
|
+
var TIMEOUT_MS = 3e4;
|
|
60
|
+
var DEFAULT_NAME = "workspace";
|
|
61
|
+
async function runWorkspaceCommand(args) {
|
|
62
|
+
switch (args.subcommand) {
|
|
63
|
+
case "details":
|
|
64
|
+
return runDetails(args);
|
|
65
|
+
case "export":
|
|
66
|
+
return runExport(args);
|
|
67
|
+
case "codegen":
|
|
68
|
+
return runCodegenCommand(args, { kind: "workspace" });
|
|
69
|
+
case "deploy":
|
|
70
|
+
throw removedSubcommand("workspace", "deploy");
|
|
71
|
+
default:
|
|
72
|
+
throw unknownSubcommand("workspace", args.subcommand);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function fetchWorkspaces(auth) {
|
|
76
|
+
const url = new URL("/api:meta/workspace", auth.instance);
|
|
77
|
+
const res = await fetchOrExplain(
|
|
78
|
+
url.href,
|
|
79
|
+
{
|
|
80
|
+
headers: { accept: "application/json", Authorization: `Bearer ${auth.access_token}` },
|
|
81
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
82
|
+
},
|
|
83
|
+
"workspace list",
|
|
84
|
+
TIMEOUT_MS
|
|
85
|
+
);
|
|
86
|
+
const text = await res.text();
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(httpFailure("workspace list", res, text));
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(text);
|
|
92
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error(`workspace list: could not parse the response as JSON:
|
|
95
|
+
${text.slice(0, 200)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function runDetails(args) {
|
|
99
|
+
const auth = await getAccessToken(args);
|
|
100
|
+
const workspaceId = auth.workspaceId;
|
|
101
|
+
const all = await fetchWorkspaces(auth);
|
|
102
|
+
const match = all.find((w) => w.id === workspaceId);
|
|
103
|
+
if (!match) {
|
|
104
|
+
const known = all.filter((w) => typeof w.id === "number").map((w) => ` ${String(w.id).padStart(3)} ${typeof w.name === "string" ? w.name : "(unnamed)"}`).join("\n");
|
|
105
|
+
const source = auth.credentialType !== "token" ? `Run \`xanots login\` again to re-pin it` : process.env[ENV_WORKSPACE] !== void 0 ? `Fix \`${ENV_WORKSPACE}\` in the environment` : `Fix \`workspace_id\` in your credential file`;
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Workspace ${workspaceId} does not exist on ${new URL(auth.instance).host} (or your credential cannot see it).
|
|
108
|
+
${source}.` + (known === "" ? "" : `
|
|
109
|
+
|
|
110
|
+
Workspaces you can reach:
|
|
111
|
+
${known}`)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const summary = {
|
|
115
|
+
instance: auth.instance,
|
|
116
|
+
id: workspaceId,
|
|
117
|
+
name: typeof match.name === "string" ? match.name : void 0,
|
|
118
|
+
guid: typeof match.guid === "string" ? match.guid : void 0,
|
|
119
|
+
/** Which credential this command is acting under — the only thing that selects a workspace. */
|
|
120
|
+
credential: auth.credentialType
|
|
121
|
+
};
|
|
122
|
+
const microservices = await readMicroservices(auth, auth.instance, workspaceId);
|
|
123
|
+
if (isMachineOutput(args)) {
|
|
124
|
+
writeJson({ ...summary, microservices });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const s = stdoutStyle();
|
|
128
|
+
const rows = [
|
|
129
|
+
["instance", summary.instance],
|
|
130
|
+
["workspace", `${summary.name ?? "(unnamed)"} ${s.dim(`#${summary.id}`)}`]
|
|
131
|
+
];
|
|
132
|
+
if (summary.guid !== void 0) rows.push(["guid", summary.guid]);
|
|
133
|
+
rows.push([
|
|
134
|
+
"source",
|
|
135
|
+
summary.credential === "token" ? "your meta API token credential" : "your sign-in (pinned at login)"
|
|
136
|
+
]);
|
|
137
|
+
process.stdout.write(formatFields(rows) + "\n");
|
|
138
|
+
printMicroserviceSection(microservices);
|
|
139
|
+
}
|
|
140
|
+
async function runExport(args) {
|
|
141
|
+
const auth = await getAccessToken(args);
|
|
142
|
+
const bundle = await fetchWorkspaceBundle(auth);
|
|
143
|
+
const content = JSON.stringify(bundle, null, 2);
|
|
144
|
+
const target = resolveOutputTarget({ path: args.path, name: args.name ?? DEFAULT_NAME, ext: "json" });
|
|
145
|
+
if (target.kind === "stdout") {
|
|
146
|
+
process.stdout.write(content + "\n");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
writeFileSync(target.path, content + "\n", "utf8");
|
|
150
|
+
step(`Exported workspace \u2192 ${target.path}`);
|
|
151
|
+
success(`Wrote ${target.path}`);
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
runWorkspaceCommand
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=workspace-command-OF23DPNI.js.map
|
package/llms/fields.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Column and input types
|
|
2
|
+
|
|
3
|
+
> Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and accessor methods, and the `s.precondition` error/status contract that rides the same catalog.
|
|
4
|
+
|
|
5
|
+
Author table columns + function/API inputs with the typed catalog: `f.<type>(opts?)`
|
|
6
|
+
for columns, `input.<type>(opts?)` for inputs. Common opts: `required`, `nullable`,
|
|
7
|
+
`default`, `description`.
|
|
8
|
+
**`nullable` defaults PER TYPE, matching the engine's own column-creation API**: `true`
|
|
9
|
+
for `f.vector`, `f.uuid`, every `f.geo.*` and every file type (`f.image`/`f.video`/
|
|
10
|
+
`f.audio`/`f.attachment`), `false` for everything else (text, int, decimal, bool, email,
|
|
11
|
+
enum, json, object, password, date, tableRef). Pass `nullable` explicitly to override —
|
|
12
|
+
e.g. `f.geo.polygon({ nullable: false })`. This is why `f.vector(8)` deploys: the engine
|
|
13
|
+
turns an empty default into SQL NULL only for a nullable column, so a non-null vector
|
|
14
|
+
would reach PostgreSQL as `vector(8) not null default ''` and fail to create.
|
|
15
|
+
**`f.geo.*` values are `{ type, data }`, not GeoJSON.** The same shape goes in and comes
|
|
16
|
+
back: `{ type: "point", data: { lng, lat } }`, `{ type: "poly", data: [{ lng, lat }, …] }`
|
|
17
|
+
— `type` is the engine's abbreviation, and a polygon ring is closed for you. Raw WKT text
|
|
18
|
+
(`c.text("POINT(1 2)")`) is accepted on write too, but a read never returns one.
|
|
19
|
+
`methods` is a bind-time validator/transform pipeline whose
|
|
20
|
+
valid names depend on the field type (below) — pass bare names (`"trim"`), the
|
|
21
|
+
colon-form with args (`"min:8"`), or `{ name, arg }` for anything not listed.
|
|
22
|
+
`f.json({children})` declares the nested shape stored INSIDE a json column — an ARRAY of
|
|
23
|
+
`{name, type, methods?, children?}`, order-significant, distinct from the `FieldMap` that
|
|
24
|
+
`f.object` takes positionally. Omit it for an unstructured json column.
|
|
25
|
+
`f.enum(values)`/`f.vector(size)`/`f.object(children)`/`f.tableRef(table)` take a
|
|
26
|
+
positional payload before opts — and still accept the standard `FieldOptions` after it
|
|
27
|
+
(`f.enum([])`/`input.enum([])` are accepted, because the engine stores an enum whose
|
|
28
|
+
options were never filled in — that is a pulled-workspace shape, not one to author; it
|
|
29
|
+
brands the column `never`, which `InferRow` surfaces at read time as `undefined` — so
|
|
30
|
+
code written against `never` is dead in a way `undefined` is not.)
|
|
31
|
+
(e.g. `f.tableRef(users, { required: true })` — only `min`/`max` are listed as tableRef
|
|
32
|
+
methods below, but `required`/`nullable`/`description`/… apply like any field.)
|
|
33
|
+
An **OPTIONAL foreign key wants a `0` sentinel, not `nullable: true`.** `f.tableRef` stores
|
|
34
|
+
an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/`id`, so
|
|
35
|
+
`s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather
|
|
36
|
+
than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for
|
|
37
|
+
"not set yet" — `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and
|
|
38
|
+
binds `null`, which is the answer the null was reaching for. `export()` warns on a literal
|
|
39
|
+
`c.null()` in that slot.
|
|
40
|
+
An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through
|
|
41
|
+
any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:
|
|
42
|
+
"vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see
|
|
43
|
+
`s.db.query` → `eval`). Without that pairing the column stores and indexes but nothing
|
|
44
|
+
queries it.
|
|
45
|
+
`{ array: true }` makes any `f.*` scalar a **list column** — `f.text({ array: true })`
|
|
46
|
+
surfaces as `string[]` in `InferRow<typeof table>` (the column analogue of `input.list`).
|
|
47
|
+
A **column `default` must stay within the BMP** — a 4-byte character (codepoint > U+FFFF,
|
|
48
|
+
e.g. an emoji) is mangled into invalid UTF-8 by the engine's default pipeline and is rejected
|
|
49
|
+
at export rather than 500ing at deploy (Postgres `22021`); BMP defaults (accents, `€`, most
|
|
50
|
+
CJK) are fine, or put the value on an `input.<type>({ default })`, applied at runtime bind.
|
|
51
|
+
`input.*` mirrors `f.*` — every column type below is
|
|
52
|
+
a legal input (scalars, files `input.image/video/audio/attachment`, `input.geo.*`,
|
|
53
|
+
`input.vector(size)`, `input.tableRef(table)`, `input.object(children)`), plus
|
|
54
|
+
`input.dbLink(table)` is the odd one: ONE entry that EXPANDS into one input per
|
|
55
|
+
COLUMN of the linked table, so read them by column name (`inp("email")`), never by
|
|
56
|
+
the entry's own name. `hidden: ["created_at"]` drops columns from that expansion.
|
|
57
|
+
`input.list(element)` for arrays — wrap any element constructor, e.g.
|
|
58
|
+
`input.list(input.text())` or `input.list(input.object({ id: f.int() }))`. Prefer the
|
|
59
|
+
typed forms over `input.json()` when the shape is known.
|
|
60
|
+
**Typed inputs validate/coerce on bind, before your stack runs** — so reach for the
|
|
61
|
+
specific type instead of hand-rolling checks. `input.email({ required: true })` rejects a
|
|
62
|
+
malformed address with a 400 (and trims; add `methods: ["lower"]` to downcase) — no
|
|
63
|
+
`regex_matches` needed; `input.int`/`input.decimal`/`input.uuid`/`input.enum([...])`/`input.date`
|
|
64
|
+
likewise reject or coerce bad input at the boundary. Drop to `input.text` + `s.precondition`
|
|
65
|
+
only for rules no type expresses.
|
|
66
|
+
⚠ `input.url` is NOT one of them — there is no engine `url` type, so it stores as `text`
|
|
67
|
+
and validates NOTHING: a `javascript:`/`data:` URL type-checks, imports, and binds. It
|
|
68
|
+
names intent and carries the `text` methods, nothing more. When the value gets navigated
|
|
69
|
+
to, check the scheme in the stack. It is INPUT ONLY — there is no `f.url` column.
|
|
70
|
+
⚠ `s.precondition`'s `error` must be a TAGGED value — `c.text("…")`, not a bare string.
|
|
71
|
+
The engine falls back to the generic "Precondition failed." whenever it reads an empty or
|
|
72
|
+
non-scalar message, and a bare string lands there, so the client never sees your text. The
|
|
73
|
+
`error_type` → HTTP status mapping is correct either way; only the message is lost. The bare
|
|
74
|
+
form stays accepted so a pulled workspace round-trips, not as a spelling to choose.
|
|
75
|
+
`error_type` IS how a stack sets a response status: `badrequest`/`inputerror` → 400, `unauthorized` → 401, `accessdenied` → 403, `notfound` → 404, `toomanyrequests` → 429, `standard` → 500.
|
|
76
|
+
Normalizing transforms run on bind too — put `trim`/`lower`/`upper` on the input's `methods`
|
|
77
|
+
so `inp("name")` reads already-normalized; don't reroll `var $x = inp("name")|trim` in the stack.
|
|
78
|
+
|
|
79
|
+
- `f.text` — methods: alphaOk, digitOk, lower, max, min, ok, pattern, startsWith, trim, upper
|
|
80
|
+
- `f.int` — methods: max, min
|
|
81
|
+
- `f.decimal` — methods: max, min
|
|
82
|
+
- `f.bool`
|
|
83
|
+
- `f.uuid`
|
|
84
|
+
- `f.date`
|
|
85
|
+
- `f.email` — methods: lower, trim
|
|
86
|
+
- `f.password` — methods: max, min, minAlpha, minDigit, minLowerAlpha, minSymbol, minUpperAlpha, salt
|
|
87
|
+
- `f.json`
|
|
88
|
+
- `f.timestamp` (stored `epochms`)
|
|
89
|
+
- `f.image` (stored `blob_img`)
|
|
90
|
+
- `f.video` (stored `blob_video`)
|
|
91
|
+
- `f.audio` (stored `blob_audio`)
|
|
92
|
+
- `f.attachment` (stored `blob`)
|
|
93
|
+
- `input.file` — INPUT ONLY (no `f.` form)
|
|
94
|
+
- `input.dbLink` (stored `<tableGuid>_mvpschema`) — INPUT ONLY (no `f.` form)
|
|
95
|
+
- `f.geo.point` (stored `geo_point`)
|
|
96
|
+
- `f.geo.multipoint` (stored `geo_multipoint`)
|
|
97
|
+
- `f.geo.linestring` (stored `geo_linestring`)
|
|
98
|
+
- `f.geo.multilinestring` (stored `geo_multilinestring`)
|
|
99
|
+
- `f.geo.polygon` (stored `geo_polygon`)
|
|
100
|
+
- `f.geo.multipolygon` (stored `geo_multipolygon`)
|
|
101
|
+
- `f.enum`
|
|
102
|
+
- `f.vector` — methods: max, min
|
|
103
|
+
- `f.object` (stored `obj`)
|
|
104
|
+
- `f.tableRef` (stored `int`) — methods: max, min
|
package/llms/filters.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Filter catalog
|
|
2
|
+
|
|
3
|
+
> Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
4
|
+
|
|
5
|
+
Attach to a value with `withFilters(v, fl.name(...))` — the value `filters[]`
|
|
6
|
+
pipeline. Filters are passed spread (canonical); the array form
|
|
7
|
+
`withFilters(v, [fl.a(), fl.b()])` is also accepted. Every filter is typed:
|
|
8
|
+
the ones below carry named args, and the rest take NO arguments — call them
|
|
9
|
+
`fl.<name>()`, and passing an argument is a compile error.
|
|
10
|
+
A typed filter's declared argument list is an EXACT count, not a floor: passing more
|
|
11
|
+
than it lists THROWS, in the type and at runtime. The extra argument used to ride into
|
|
12
|
+
the filter's arg list, where the engine either ignores it or fails opaquely on a live
|
|
13
|
+
endpoint. `filter("name", …)` is the untyped escape for a filter the catalog
|
|
14
|
+
under-declares. Seven declared filters are exceptions the engine really does take more from, so they
|
|
15
|
+
stay variadic: `concat` (trailing separator), `index_by` (trailing list flag), `get` (trailing variable
|
|
16
|
+
map), `array_merge` and `array_merge_recursive` (any number of arrays), `jwe_encode` and
|
|
17
|
+
`jwe_decode`. To send more than a zero-argument filter's `()` accepts, use the same
|
|
18
|
+
`filter("name", …)` escape. A filter with NO declared list takes nothing and is emitted
|
|
19
|
+
`()` — except the variadic few listed under the catalog, which take arguments the
|
|
20
|
+
catalog never declared and so are not arity-checked at all.
|
|
21
|
+
A typed filter also accepts one object of NAMED arguments (`fl.add({ value: 1 })`); a key
|
|
22
|
+
the filter does not declare throws rather than being dropped.
|
|
23
|
+
A bare JS **scalar** — string, number or boolean — is accepted in ANY `fl.*` argument and
|
|
24
|
+
wrapped as the constant you would have written by hand (`fl.get("a.b", 0)` encodes
|
|
25
|
+
identically to `fl.get(c.text("a.b"), c.int(0))`). An object or array must still be built
|
|
26
|
+
with `c.obj`/`c.array`. The arg types below name each argument's ENGINE type, not the JS
|
|
27
|
+
type you may pass.
|
|
28
|
+
Read-modify-write a column from its current value with the pipeline: to increment
|
|
29
|
+
a counter you MUST `db.get` the row first, then pipe its bound value —
|
|
30
|
+
`col("clicks")` does NOT resolve to the stored value inside a `db.edit` `row`
|
|
31
|
+
(it is `null`, so `fl.add(1)` computes `null + 1` and the engine aborts):
|
|
32
|
+
`s.db.get({ table, fieldValue, as: "current" })` then
|
|
33
|
+
`s.db.edit({ table, fieldValue, row: { clicks: withFilters(ref("current.clicks"), fl.add(c.int(1))) } })`.
|
|
34
|
+
⚠ This read-modify-write is NOT atomic — two concurrent writers can both read the
|
|
35
|
+
same value and one increment is lost. There is no dedicated atomic-increment
|
|
36
|
+
statement, and one CANNOT be synthesized in the SDK (it would compile to this same
|
|
37
|
+
`get` + `edit` pair). For a **concurrency-safe** counter, do the arithmetic in the
|
|
38
|
+
database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHERE …`),
|
|
39
|
+
which the DB applies atomically. Reserve the pipeline form for low-contention counters
|
|
40
|
+
where a rare lost update is acceptable.
|
|
41
|
+
⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
|
|
42
|
+
does NOT expose: the engine derives a physical name from workspace + table ids (of the
|
|
43
|
+
form `x<workspace_id>_<table_id>`, e.g. `x6_203970`), ids assigned at import — not knowable
|
|
44
|
+
from a `table()` def (identity is a name + guid, not the numeric id), and `sql_name`
|
|
45
|
+
persists empty. So the safe counter drops out of the typed surface: hardcode
|
|
46
|
+
the physical name after inspecting the deployed table. A typed atomic path needs an
|
|
47
|
+
engine change.
|
|
48
|
+
|
|
49
|
+
- `fl.add(value: decimal): decimal`
|
|
50
|
+
- `fl.append(value: <T>, path: text): <T>[]`
|
|
51
|
+
- `fl.array_diff(value: <T>[]): <T>[]`
|
|
52
|
+
- `fl.array_diff_assoc(value: <T>[]): <T>[]`
|
|
53
|
+
- `fl.array_fill(start: int, count: int): any[]`
|
|
54
|
+
- `fl.array_fill_keys(keys: any[]): any[]`
|
|
55
|
+
- `fl.array_intersect(value: <T>[]): <T>[]`
|
|
56
|
+
- `fl.array_intersect_assoc(value: <T>[]): <T>[]`
|
|
57
|
+
- `fl.array_merge(value: <T>[]): <T>[]`
|
|
58
|
+
- `fl.array_merge_recursive(value: <T>[]): <T>[]`
|
|
59
|
+
- `fl.array_push(value: <T>): <T>[]`
|
|
60
|
+
- `fl.array_remove(value: <T>, path: text, strict?: bool): <T>[]`
|
|
61
|
+
- `fl.array_slice(offset: int, length?: int): <T>[]`
|
|
62
|
+
- `fl.array_unshift(value: <T>): <T>[]`
|
|
63
|
+
- `fl.base_convert(from_base: any, to_base: any): text`
|
|
64
|
+
- `fl.bitwise_and(value: int): int`
|
|
65
|
+
- `fl.bitwise_or(value: int): int`
|
|
66
|
+
- `fl.bitwise_xor(value: int): int`
|
|
67
|
+
- `fl.concat(value: any): text`
|
|
68
|
+
- `fl.contains(search: text): bool` — piped value is the subject text; the arg is the substring searched for
|
|
69
|
+
- `fl.convert_encoding(to: text, from: text): text`
|
|
70
|
+
- `fl.create_object(values: <T>[]): any`
|
|
71
|
+
- `fl.crypto_jwe_decode(check_claims: json, key: json, key_algorithm: enum, content_algorithm: enum, timeDrift?: int): json`
|
|
72
|
+
- `fl.crypto_jwe_encode(headers: json, key: json, key_algorithm: enum, content_algorithm: enum, ttl?: int): text`
|
|
73
|
+
- `fl.crypto_jws_decode(check_claims: json, key: json, algorithm: enum, timeDrift?: int): json`
|
|
74
|
+
- `fl.crypto_jws_encode(headers: json, key: json, algorithm: enum, ttl?: int): text`
|
|
75
|
+
- `fl.csv_create(rows: text[], separator: text, enclosure: text, escape: text): text` — the header-writing counterpart to `csv_encode`: the PIPED value is the list of column names (written as the header line) and `rows` carries the data rows
|
|
76
|
+
- `fl.csv_decode(separator: text, enclosure: text, escape: text): any`
|
|
77
|
+
- `fl.csv_encode(separator: text, enclosure: text, escape: text): text` — writes NO header — values only, each row in THAT row's key order with no normalization across rows, so rows whose keys differ in order or count silently misalign columns. Nested cells are JSON-encoded and `false` writes empty. A piped array of SCALARS is treated as one row. Use `csv_create` for a header
|
|
78
|
+
- `fl.csv_parse(separator: text, enclosure: text, escape: text): any`
|
|
79
|
+
- `fl.decrypt(algorithm: enum, key: text, iv: text): text`
|
|
80
|
+
- `fl.detect_encoding(encodings?: text): text`
|
|
81
|
+
- `fl.div(value: decimal): decimal`
|
|
82
|
+
- `fl.encrypt(algorithm: enum, key: text, iv: text): text`
|
|
83
|
+
- `fl.ends_with(search: text): bool` — piped value is the subject text; the arg is the substring searched for
|
|
84
|
+
- `fl.epochms_add_ms(milliseconds: int): epochms`
|
|
85
|
+
- `fl.epochms_add_secs(seconds: int): epochms`
|
|
86
|
+
- `fl.epochms_date(format: text, timezone?: text): text`
|
|
87
|
+
- `fl.epochms_from_format(format: text, timezone?: text): text`
|
|
88
|
+
- `fl.epochms_transform(format: text, timezone?: text): text` — applies a relative shift (e.g. "+1 day") to the timestamp
|
|
89
|
+
- `fl.eq(value: <T>): bool`
|
|
90
|
+
- `fl.every(code: text, timeout?: int): any[]` — `code` is a JS body run per element (true for all?), over `$this`/`$index`/`$parent`
|
|
91
|
+
- `fl.filter(code: text, timeout?: int): any[]` — `code` is a JS body run per element (keep it? true/false), over `$this`/`$index`/`$parent`
|
|
92
|
+
- `fl.filter_empty(path?: text): <T>[]` — keeps entries that are not empty ("", null, 0, "0", false, [], {})
|
|
93
|
+
- `fl.filter_empty_array(path?: text): <T>[]`
|
|
94
|
+
- `fl.filter_empty_object(path?: text): <T>[]`
|
|
95
|
+
- `fl.filter_empty_text(path?: text): <T>[]`
|
|
96
|
+
- `fl.filter_false(path?: text): <T>[]`
|
|
97
|
+
- `fl.filter_null(path?: text): <T>[]`
|
|
98
|
+
- `fl.filter_zero(path?: text): <T>[]`
|
|
99
|
+
- `fl.find(code: text, timeout?: int): any[]` — `code` is a JS body run per element; returns the first element it accepts
|
|
100
|
+
- `fl.findIndex(code: text, timeout?: int): any[]` — `code` is a JS body run per element; returns the first matching index
|
|
101
|
+
- `fl.first_notempty(value: any): any` — first value that is not empty ("", null, 0, "0", false, [], {})
|
|
102
|
+
- `fl.first_notnull(value: any): any`
|
|
103
|
+
- `fl.fsort(path?: text, type?: "text"|"itext"|"natural"|"inatural"|"number", asc?: bool): <T>[]` — `type` is the comparator, and ONLY "number" compares numerically — "text"/"itext" are strcmp/strcasecmp, "natural"/"inatural" are the human-readable "a2 < a10" orderings. Default "itext". Anything else silently sorts as text, so a numeric sort MUST spell "number"; the path arg drills into each element
|
|
104
|
+
- `fl.get(path: text, default?: json): any`
|
|
105
|
+
- `fl.gt(value: <T>): bool`
|
|
106
|
+
- `fl.gte(value: <T>): bool`
|
|
107
|
+
- `fl.has(path: text): bool`
|
|
108
|
+
- `fl.hmac_md5(key: text, raw?: bool): text`
|
|
109
|
+
- `fl.hmac_sha1(key: text, raw?: bool): text`
|
|
110
|
+
- `fl.hmac_sha256(key: text, raw?: bool): text`
|
|
111
|
+
- `fl.hmac_sha384(key: text, raw?: bool): text`
|
|
112
|
+
- `fl.hmac_sha512(key: text, raw?: bool): text`
|
|
113
|
+
- `fl.icontains(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
|
|
114
|
+
- `fl.iends_with(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
|
|
115
|
+
- `fl.in(search: <T>): bool`
|
|
116
|
+
- `fl.index_by(path: text): { [key: string]: <T>[] }` — a GROUP-BY: every value is an ARRAY of the items sharing that key, even when only one does, so a lookup reads `idx[key][0]`. Items whose path is missing or non-scalar are dropped
|
|
117
|
+
- `fl.istarts_with(search: text): bool` — case-insensitive; piped value is the subject, the arg is the substring
|
|
118
|
+
- `fl.join(separator: text): text`
|
|
119
|
+
- `fl.jwe_decode(arg1: any)`
|
|
120
|
+
- `fl.jwe_encode(arg1: any)`
|
|
121
|
+
- `fl.lambda(code: text, timeout?: int): any` — runs a JS body once over the piped value, which it binds as `$this` (NOT `$parent`)
|
|
122
|
+
- `fl.log(base: <T>): decimal`
|
|
123
|
+
- `fl.lt(value: <T>): bool`
|
|
124
|
+
- `fl.lte(value: <T>): bool`
|
|
125
|
+
- `fl.ltrim(mask?: text): text`
|
|
126
|
+
- `fl.map(code: text, timeout?: int): any[]` — `code` is a JS body run per element, over `$this`/`$index`/`$parent` — build it with `lam.fn`
|
|
127
|
+
- `fl.md5(raw?: bool): text`
|
|
128
|
+
- `fl.mod(value: int): int`
|
|
129
|
+
- `fl.mul(value: decimal): decimal`
|
|
130
|
+
- `fl.ne(value: <T>): bool`
|
|
131
|
+
- `fl.num_max(value: any): decimal`
|
|
132
|
+
- `fl.num_min(value: any): decimal`
|
|
133
|
+
- `fl.number_format(decimals: int, decimal_separator: text, thousands_separator: text): string`
|
|
134
|
+
- `fl.pick(keys: text): <T>`
|
|
135
|
+
- `fl.pow(exp: <T>): decimal`
|
|
136
|
+
- `fl.prepend(value: <T>, path: text): <T>[]`
|
|
137
|
+
- `fl.range(start: int, stop: int): int[]`
|
|
138
|
+
- `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
|
|
139
|
+
- `fl.regex_match(subject: text): text[]`
|
|
140
|
+
- `fl.regex_match_all(subject: text): text[]`
|
|
141
|
+
- `fl.regex_quote(delimiter?: text): text`
|
|
142
|
+
- `fl.regex_replace(replacement: text, subject: text): text`
|
|
143
|
+
- `fl.regex_test(subject: text): bool`
|
|
144
|
+
- `fl.round(precision?: int): decimal`
|
|
145
|
+
- `fl.rtrim(mask?: text): text`
|
|
146
|
+
- `fl.secureid_decode(salt: text): int`
|
|
147
|
+
- `fl.secureid_encode(salt: text): text`
|
|
148
|
+
- `fl.set(path: text, value: any): any`
|
|
149
|
+
- `fl.set_conditional(path: text, value: any, conditional: any): any`
|
|
150
|
+
- `fl.set_ifnotempty(path: text, value: any): any`
|
|
151
|
+
- `fl.set_ifnotnull(path: text, value: any): any`
|
|
152
|
+
- `fl.sha1(raw?: bool): text`
|
|
153
|
+
- `fl.sha256(raw?: bool): text`
|
|
154
|
+
- `fl.sha384(raw?: bool): text`
|
|
155
|
+
- `fl.sha512(raw?: bool): text`
|
|
156
|
+
- `fl.some(code: text, timeout?: int): any[]` — `code` is a JS body run per element (true for any?), over `$this`/`$index`/`$parent`
|
|
157
|
+
- `fl.split(separator: text): text[]`
|
|
158
|
+
- `fl.starts_with(search: text): bool` — piped value is the subject text; the arg is the substring searched for
|
|
159
|
+
- `fl.string_replace(search: text, replacement: text): text`
|
|
160
|
+
- `fl.strip_tags(exclude?: text): text`
|
|
161
|
+
- `fl.stripos(search: text): int`
|
|
162
|
+
- `fl.strpos(search: text): int`
|
|
163
|
+
- `fl.sub(value: decimal): decimal`
|
|
164
|
+
- `fl.substr(start: int, length: int): text`
|
|
165
|
+
- `fl.to_epoch_day(timezone?: text): int`
|
|
166
|
+
- `fl.to_epoch_hour(timezone?: text): int`
|
|
167
|
+
- `fl.to_epoch_minute(timezone?: text): int`
|
|
168
|
+
- `fl.to_epoch_ms(timezone?: text): int`
|
|
169
|
+
- `fl.to_epoch_sec(timezone?: text): int`
|
|
170
|
+
- `fl.to_epochms(timezone?: text): epochms`
|
|
171
|
+
- `fl.transform(expression: text): any` — `expression` is Xano Expression Engine source, NOT a JS body — no `return`, and the piped value is `$0` (or `$$`), NOT `$this` (which is null here). `$var`/`$input`/`$env`/`$auth` resolve and filters pipe inside it: `$0 * 2`, `$0|sort|join:","`. Parenthesize a pipe inside an object literal — `{ s: ($0|sort|join:",") }` — or its comma is read as the key separator and later keys vanish silently. For JavaScript use `lambda`
|
|
172
|
+
- `fl.trim(mask?: text): text`
|
|
173
|
+
- `fl.unique(path?: text): <T>[]`
|
|
174
|
+
- `fl.unpick(keys: text): <T>` — returns the object without the named keys (inverse of a pick)
|
|
175
|
+
- `fl.unset(path: text): any`
|
|
176
|
+
- `fl.url_addarg(key: text, value: text, encoding_rfc3986?: bool): text`
|
|
177
|
+
- `fl.url_delarg(key: text): text`
|
|
178
|
+
- `fl.url_getarg(key: text, default?: text): text`
|
|
179
|
+
- `fl.url_hasarg(key: text): text`
|
|
180
|
+
|
|
181
|
+
Zero-argument filters — call as `fl.<name>()`; an argument is a compile error: abs, acos, acosh, addslashes, array_entries, array_keys, array_pop, array_shift, array_shuffle, array_values, asin, asinh, atan, atanh, avg, base64_decode, base64_decode_urlsafe, base64_encode, base64_encode_urlsafe, bin2hex, bindec, bitwise_not, capitalize, ceil, cos, count, create_object_from_entries, decbin, dechex, decoct, deg2rad, empty, escape, even, exp, first, flatten, floor, from_utf8, hex2bin, hexdec, is_array, is_bool, is_decimal, is_int, is_object, is_text, is_uuid, json_decode, json_encode, last, list_encodings, ln, log10, lower, max, min, not, null, octdec, odd, product, querystring_parse, rad2deg, reverse, safe_array, sin, sql_alias, sql_esc, sqrt, strip_accents, strlen, sum, tan, text_escape, text_unescape, to_bool, to_decimal, to_expr, to_int, to_text, to_utf8, uid, upper, url_decode, url_decode_rfc3986, url_encode, url_encode_rfc3986, url_parse, uuid4, xml_decode, yaml_decode, yaml_encode.
|
|
182
|
+
|
|
183
|
+
Variadic filters — they take arguments, but no declared list, so the count is not checked: sort, sprintf.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Agent and MCP def shapes
|
|
2
|
+
|
|
3
|
+
> Read when the workspace defines an `agent()` or an `mcpServer()`.
|
|
4
|
+
|
|
5
|
+
- `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` — an MCP toolset. `llm?`/`output?` are the same blocks `agent()` takes and are usually absent: an MCP server and an agent are ONE stored object distinguished by `type`, so a server that carries LLM settings can say so. Returns a handle with `getPath()`/`getUrl(baseUrl)` for the Streamable-HTTP endpoint — `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<…>/stream` path (an earlier result) THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral; does NOT fire in the sandbox (see Gotchas).
|
|
6
|
+
- `tools?`: a `ToolsetToolEntry[]`. Pass the `tool()` HANDLES directly (`tools: [saveNote]`), like every other collection in the SDK; use the `{ tool, enabled?, auth? }` wrapper only when a tool needs `enabled: false` or per-tool `auth`. `auth` names an auth **table** (a `table({ auth: true })` handle or its name) — Xano's ONLY MCP auth surface (per-tool; there is no server-level gate). An entry that names no tool (no handle, no `id`) THROWS at export rather than emitting the `id: 0` null reference it used to; a deliberate raw `id: 0` warns and is carried through, so a pulled workspace still round-trips.
|
|
7
|
+
- `agent({ name, guid?, description?, docs?, enabled?, canonical?, tags?, history?, llm, tools?, output? })` — an LLM orchestrator. No top-level `instructions`/`prompt`/`spec` — the prompt lives under `llm`. Invoke from a stack with `s.ai.agent.run({ agent, args })`.
|
|
8
|
+
- `llm` (REQUIRED): typed provider settings, a discriminated union on `type` (`"xano-free" | "anthropic" | "openai" | "google-genai"`). Shared fields: `systemPrompt?`, `maxSteps?` (default `5`), and `prompt?` XOR `messages?` (genuinely exclusive: both is a type error and throws — the engine stores ONE `prompt_type`, so one would be dropped); plus provider fields (`apiKey?`, `model?`, `temperature?`, `reasoningEffort?`, …). String fields accept Twig placeholders — `{{ $args.x }}` for run inputs (the `args` of `s.ai.agent.run`), `{{ $env.NAME }}` for env vars.
|
|
9
|
+
- `tools?`: same `ToolsetToolEntry[]` as `mcpServer` — bare `tool()` handles, or a `{ tool, enabled?, auth? }` wrapper when one needs `enabled`/`auth`.
|
|
10
|
+
- `output?`: `{ schema: Record<string, input.*>, enabled? }` — structured-output schema. `schema` is a named-field record authored with the `input.*` catalog, exactly like a `defineFunction`/`query` `input:` map (the stored `structuredOutputsSchema` is the same wire shape as function inputs). e.g. `output: { schema: { priority: input.enum(["low","high"]), summary: input.text() } }`. When you pass the agent *handle* to `s.ai.agent.run({ agent })`, `.result` is typed straight from this schema — no `resultShape` witness needed (the shape is declared once). `resultShape` remains only to override that, or to type an agent referenced by bare name.
|
|
11
|
+
- **Run + read recipe (`s.ai.agent.run`):** bind the run to a var (`s.ai.agent.run({ agent, args, as: "run" })`) — it produces a rich envelope, and the completion is at **`.result`**. Read one structured field with a dotted ref (`response: ref("run.result.priority")`) or the whole completion (`ref("run.result")`, typed from `output.schema`); persist it in a later step the same way (`s.set_var({ name, value: ref("run.result.summary") })`). `args` is a plain object of run inputs (`{ topic: inp("topic") }` — raw literals are fine, e.g. `{ max_steps: 3 }`) surfaced to the agent as `{{ $args.topic }}`. Tool-call / per-step data (when tools ran) rides `.toolCalls` / `.steps` on the same envelope — both optional, absent or empty when no tools executed.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Core def shapes
|
|
2
|
+
|
|
3
|
+
> Read when authoring a function, query, api group, task, workflow test, middleware, or tool — and for the `response` and `expr` shapes every one of them uses.
|
|
4
|
+
|
|
5
|
+
The def-object passed to each factory. `?` = optional. `input` is keyed by
|
|
6
|
+
input name (`input.<type>(opts?)`); `stack` is `Statement[]` (`s.*`); `response`
|
|
7
|
+
is a `ResponseDef` (see **Responses** below). Object identity is `guid?` —
|
|
8
|
+
omit it and it derives from `name` (set it to survive a rename).
|
|
9
|
+
|
|
10
|
+
- `defineFunction({ name, guid?, description?, docs?, workspace?, input?, stack?, response?, tests? })`
|
|
11
|
+
- `query({ name, verb, apiGroup?, guid?, auth?, input?, stack?, response?, responseType?, apiEnabled?, disabled?, cache?, description?, docs?, tests?, example? })`
|
|
12
|
+
- `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
|
|
13
|
+
- `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
|
|
14
|
+
- `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
|
|
15
|
+
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
16
|
+
- **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
|
|
17
|
+
- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
|
|
18
|
+
- `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
|
|
19
|
+
- `mode?`: `"default"` (the default) | `"custom"` | `"disabled"`, lowercase — a fourth value THROWS at export (`apiGroup()` itself does not check), because Xano neither rejects nor blanks it: it DROPS THE WHOLE API GROUP on import, so the deploy succeeds and every query in the group 404s. ⚠ Every OTHER field applies only under `"custom"`: `"default"` serves a FIXED permissive policy (any origin, `allow-headers: *`, `allow-credentials: true`, `max-age: 86400`) and ignores the block, so setting `maxAge`/`allowCredentials`/`allowHeaders` alone changes nothing. `"disabled"` sends no CORS headers at all, so every browser call fails.
|
|
20
|
+
- ⚠ Under `"custom"`, `allowOrigins` is matched as EXACT strings (scheme+host+port, no wildcard or subdomain expansion) and `"*"` is compared as a literal origin — it matches NOTHING. An unmatched origin gets no `access-control-*` headers at all, so the call fails in the browser on a missing `access-control-allow-origin` while export, deploy and the preflight all look fine. Name each origin, or use `mode: "default"` for any-origin. `allowMethods` gates the REAL response too: a verb left off gets no CORS headers back even though its preflight passes. Export warns on an empty origin list, a `"*"` entry, and a policy with no method enabled.
|
|
21
|
+
- `defineFunction`/`query`/`apiGroup` above cover the queries+tables core; the four below are the "reach past that" primitives (tasks, workflow tests, middleware, tools). Agents and MCP servers are the same family and live in `llms/kinds-agent-mcp.md`. Same envelope conventions (`guid?`, `description?`, `docs?`, `tags?`, `history?`) unless noted.
|
|
22
|
+
- `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` — a scheduled background job (function-like `stack`, no `input`/`response`).
|
|
23
|
+
- `schedule?`: a `ScheduleDef[]` (NOT a single object) — `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`. `startsOn`/`endsOn` are **timestamp strings** validated at encode time — `"2026-01-01T00:00:00Z"`, or the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries — never epoch numbers, and never zoneless; `freq` is the repeat interval **in seconds** (default `86400` = daily); `endsOn` present ⇒ the schedule has an end. `endsEnabled?` defaults to that and is recovery-only — state it to reproduce a stored schedule that remembers an end date with the gate OFF. Fires on an ephemeral; does NOT fire in the sandbox (see Gotchas).
|
|
24
|
+
- `workflowTest({ name, guid?, description?, docs?, datasource?, active?, tags?, stack? })` — an end-to-end test. NO `input`/`response`: `.call` something with an `as`, then assert on that var — `s.function.call({ fn, input, as: "r" })`, `s.expect.to_equal({ expr: ref("r"), value: c.int(42) })`. `s.expect.*` belongs here — it is not inert elsewhere (a failure 500s the request), so treat one in a query/function/task as a mistake to remove. `active?` defaults `true`; chain tests with `s.workflow_test.call({ workflowTest: <def handle> })`.
|
|
25
|
+
- `datasource?`: **the trap.** Default `""` is an EMPTY datasource (recommended), not "no datasource". Any non-empty name makes the engine CLONE it before EVERY run — against production-sized data, slow enough to fail the run. `"live"` warns at compile time; other names don't.
|
|
26
|
+
- `middleware({ name, guid?, description?, docs?, resultStrategy?, exceptionPolicy?, tags?, history?, input?, stack?, response?, responseShape?, tests? })` — a pre/post interceptor (function-like `stack`); attach it via a host's `middleware: { pre, post }`. ⚠ `input` ENCODES but an ATTACHED middleware never has it bound — the host request binds its own inputs, so `inp()` inside pre/post fails at runtime with `Unable to locate input` and a declared default does not stand in (`export()` warns). Read the request body with `s.util.get_all_input` instead; it yields a `{ type, vars }` envelope. `s.middleware.call` is the one path that DOES bind the declared map.
|
|
27
|
+
- `resultStrategy?`: `"merge" | "replace"` (default `merge`) — how the middleware `response` folds into the host's.
|
|
28
|
+
- `exceptionPolicy?`: `"silent" | "rethrow" | "critical"` (default `"rethrow"` — a throw ABORTS the request and surfaces the authored error/status, which is what a guard wants). `"silent"` swallows the throw and lets the request through, so a guard set to it is NOT enforced — use it only for advisory middleware. `"critical"` is `"rethrow"` plus skipping the `post` chain.
|
|
29
|
+
- `tool({ name, guid?, description?, instructions?, docs?, enabled?, tags?, history?, input?, stack?, response?, responseShape?, middleware? })` — a function-like operation (`input`/`stack`/`response`) that a toolset (MCP server or agent) exposes. Register it, then reference it from a toolset's `tools`.
|
|
30
|
+
### Responses
|
|
31
|
+
|
|
32
|
+
The `response?` field (on functions, queries, tools, middleware, and
|
|
33
|
+
response-bearing triggers) maps to the stored `result[]`:
|
|
34
|
+
|
|
35
|
+
- `ResponseDef = Value | Record<string, Value>`.
|
|
36
|
+
- A single `Value` → one unnamed result item: `response: ref("rows")`.
|
|
37
|
+
- A record → one named item per key: `response: { user: ref("u"), token: ref("t") }`.
|
|
38
|
+
- Omitted → empty `result[]` (no response body).
|
|
39
|
+
|
|
40
|
+
### Expressions (`expr`)
|
|
41
|
+
|
|
42
|
+
`expr(left, op, right)` builds the comparison used by every condition/`where`
|
|
43
|
+
surface — `s.conditional`/`s.while` `when` (incl. each `elif` branch), and
|
|
44
|
+
`db.query` `where`/`additionalWhere` (and the search triggers) — one shared tree.
|
|
45
|
+
|
|
46
|
+
- `op`: `=`, `!=`, `>`, `<`, `>=`, `<=` (JS aliases `==` `===` `!==` are accepted and normalized).
|
|
47
|
+
- `left`/`right` are `Value`s — `col("x")` (a table column), `ref`, `inp`, `auth(...)`, or `c.*`.
|
|
48
|
+
- For the full operator set (`in`/`like`/`ilike`/`between`/`contains`/`overlaps`/`@>`/`~`/`search`/…)
|
|
49
|
+
use `cmp(left, op, right, { ignoreEmpty? })`; compose nested boolean logic with `and(...)`/`or(...)`.
|
|
50
|
+
- ⚠ The wider `cmp` operators are DATABASE-only (`where`, table view filter, db trigger
|
|
51
|
+
`search`). A RUNTIME condition — `s.conditional`/`elif`, `s.while`, `s.precondition`,
|
|
52
|
+
`array.*` `if` — takes the `expr` set only; the rest are refused at build time because
|
|
53
|
+
deployed they fail the request with `Invalid op: <op>` on that branch, usually a guard.
|
|
54
|
+
Spell membership out: `or(expr(x, "=", a), expr(x, "=", b))`.
|
|
55
|
+
- A condition/`where` accepts a single `expr(...)`/`cmp(...)`, an `and()`/`or()` group, an array of
|
|
56
|
+
those (ANDed), or (for `where`) a raw `Value`. `s.conditional`/`s.while`/`s.switch`, `db.query`,
|
|
57
|
+
`precondition`, and the `array.*` predicates all take the same TREE shape (operators per above).
|
|
58
|
+
- ⚠ `mixed(a, { or: b }, { and: c })` reproduces a container whose terms do NOT all join the
|
|
59
|
+
same way — the editor allows it, so pulled workspaces contain it. **Do not author it.** The
|
|
60
|
+
stored form does not record the grouping, and the two places it can appear disagree: a
|
|
61
|
+
branch (`s.conditional`/`s.while`/`precondition`) folds terms strictly left to right, so
|
|
62
|
+
`a OR b AND c` is `(a OR b) AND c`, while a `db.query` filter applies the engine's
|
|
63
|
+
AND-before-OR precedence and selects `a OR (b AND c)`. Write `and(or(a, b), c)` or
|
|
64
|
+
`or(a, and(b, c))` — each says one reading in every context. Pulls report these as
|
|
65
|
+
`ambiguous-condition`.
|
|
66
|
+
- A **filtered** operand (`withFilters(...)`) works inline in any condition/`where` (conditional,
|
|
67
|
+
while, `db.query`/addon, …) — e.g. `cmp(withFilters(col("title"), fl.trim()), "=", inp("q"))`.
|
|
68
|
+
- e.g. `db.query({ table: posts, where: expr(col("author"), "=", auth("id")), as: "rows" })`.
|