@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
package/llms.txt
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# xanots v0.0.1
|
|
2
|
+
|
|
3
|
+
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
|
+
|
|
5
|
+
Author a Xano workspace in TypeScript: build typed objects, register them on a
|
|
6
|
+
`new Xano()` instance, and call `.export()` to get one importable bundle. Compile
|
|
7
|
+
with `xanots export ./xano/index.js`.
|
|
8
|
+
|
|
9
|
+
Coverage: object kinds 24/30, statement surfaces 214/214, filters 226 (226 typed).
|
|
10
|
+
Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; see `coverage.objectKinds.unmodeled` in `manifest.json`.
|
|
11
|
+
|
|
12
|
+
This file is the whole always-read surface: the mental model, the deploy contract,
|
|
13
|
+
every gotcha, and control flow. Per-surface detail lives in the topic files listed
|
|
14
|
+
below — open the one whose condition matches the task, and skip the rest. For
|
|
15
|
+
exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
|
|
16
|
+
defaults, a filter's complete argument list, the engine `storedName` mapping — do a
|
|
17
|
+
TARGETED lookup in the shipped `manifest.json` (grep or `jq` the one entry you need;
|
|
18
|
+
it is ~55k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
|
|
19
|
+
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
20
|
+
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
21
|
+
Select a statement on `sPath` (the `s.*` path you write), NOT `surface` (the XanoScript term): 24 of 214 differ — `var`→`set_var`, `break`→`foreach_break`, `foreach.remove`→`foreach_remove`, and every `expect.*`.
|
|
22
|
+
|
|
23
|
+
## Topic files
|
|
24
|
+
|
|
25
|
+
Each line is a condition on the task. Open the files whose condition matches and
|
|
26
|
+
skip the rest — paths are relative to this file (`node_modules/@xanots/sdk/` once
|
|
27
|
+
installed), so they resolve with a plain file read and always match the version you
|
|
28
|
+
have. `llms-full.txt` is this file and every one below concatenated — for a reader
|
|
29
|
+
that wants one fetch, not for an agent that can open the two files it needs.
|
|
30
|
+
|
|
31
|
+
- [Object kinds](llms/object-kinds.md): Read when you need the full catalog of authorable primitives — which factory builds each, which register method takes it, and which payload key it lands under. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
|
|
32
|
+
- [Core def shapes](llms/kinds-core.md): 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.
|
|
33
|
+
- [Saved unit tests, assertions, and mocks](llms/tests.md): Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement should mock a value, or when running a deployed environment's tests.
|
|
34
|
+
- [Agent and MCP def shapes](llms/kinds-agent-mcp.md): Read when the workspace defines an `agent()` or an `mcpServer()`.
|
|
35
|
+
- [Realtime def shapes](llms/kinds-realtime.md): Read when the workspace defines a `realtimeServer()`, `realtimeChannel()`, or `realtimeMessage()` handler.
|
|
36
|
+
- [Triggers](llms/triggers.md): Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.
|
|
37
|
+
- [Array and database statements](llms/statements-data.md): Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).
|
|
38
|
+
- [Statement runtime behavior](llms/statements-runtime.md): Read when you need to know what a statement's `as:` output actually holds, or why a bound variable is not the shape you expected.
|
|
39
|
+
- [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates, calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
|
|
40
|
+
- [Value catalog](llms/values.md): Read when you need a literal, a reference, or a tag you have not used before — `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.
|
|
41
|
+
- [Column and input types](llms/fields.md): 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.
|
|
42
|
+
- [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
43
|
+
- [Lambda bodies (JavaScript)](llms/lambda.md): Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`. Lambda workers are a bounded shared resource, each surface binds a different set of identifiers, and the hazards are not guessable.
|
|
44
|
+
- [Legacy paradigms and retired statements](llms/legacy.md): Read when the code was PULLED from an existing Xano instance rather than authored here — how a codegen'd tree reads and the three shapes in it that must not be "fixed", plus a `raw({ name: "mvp:…" })` statement, a `realtimeTrigger()`, or any name the catalogs do not list. None of the retired names are authorable for new code.
|
|
45
|
+
- [Statement catalog](llms/statements-catalog.md): Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace. Look here after the control-flow core in the router does not cover what you need.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
Authoring is **declarative def-objects** passed to factories — there is no
|
|
50
|
+
callback/chaining builder.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { workspace, apiGroup, query, table, input, f, ref, inp, auth, s } from "@xanots/sdk";
|
|
54
|
+
|
|
55
|
+
const users = table({
|
|
56
|
+
name: "users",
|
|
57
|
+
auth: true, // backs authentication
|
|
58
|
+
// `id` (int PK) + `created_at` (epochms) are auto-injected — don't declare them.
|
|
59
|
+
schema: {
|
|
60
|
+
email: f.email({ required: true }),
|
|
61
|
+
name: f.text(),
|
|
62
|
+
},
|
|
63
|
+
// Indexes: { type, fields: [{ name, op? }] }. `"unique"` is shorthand for `"btree|unique"`.
|
|
64
|
+
// A DOTTED name indexes a key inside a column (`xdo.email`); only the part before the
|
|
65
|
+
// first dot is checked against the schema.
|
|
66
|
+
index: [{ type: "unique", fields: [{ name: "email" }] }],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const posts = table({
|
|
70
|
+
name: "posts",
|
|
71
|
+
schema: {
|
|
72
|
+
author: f.tableRef(users), // foreign key → users (NOT `ref`)
|
|
73
|
+
body: f.text({ required: true }),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const api = apiGroup({ name: "blog", canonical: "blog" }); // canonical → the URL token
|
|
78
|
+
|
|
79
|
+
const createPost = query({
|
|
80
|
+
name: "create_post", verb: "POST", apiGroup: api, auth: users, // the auth table
|
|
81
|
+
input: { body: input.text({ required: true }) },
|
|
82
|
+
stack: [
|
|
83
|
+
s.db.add({ table: posts, row: { author: auth("id"), body: inp("body") }, as: "post" }),
|
|
84
|
+
],
|
|
85
|
+
response: ref("post"),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const listPosts = query({
|
|
89
|
+
name: "list_posts", verb: "GET", apiGroup: api,
|
|
90
|
+
stack: [s.db.query({ table: posts, sort: [{ sortBy: "created_at", dir: "desc" }], as: "rows" })],
|
|
91
|
+
response: ref("rows"),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export default workspace("my-blog")
|
|
95
|
+
.registerTables([users, posts])
|
|
96
|
+
.registerApiGroups([api])
|
|
97
|
+
.registerQueries([createPost, listPosts]);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Compile: `xanots export ./index.ts --out bundle.json` (or `writeBundle(app, path)`
|
|
101
|
+
from `@xanots/sdk/node` in code). The default export must be the `Xano` registry. The entry must be an
|
|
102
|
+
ES module (XanoTS defs are ESM-only): set `"type": "module"` in the nearest
|
|
103
|
+
package.json or name the entry `.mts`. `npm init -y` writes `"type": "commonjs"`,
|
|
104
|
+
which fails with a "must be ES modules" error until you switch it to module.
|
|
105
|
+
|
|
106
|
+
Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
|
|
107
|
+
without one a group's client paths are unresolvable until a lock exists: the bundle
|
|
108
|
+
exports fine and `xanots paths` / `getPath()` then fail on the very queries it just
|
|
109
|
+
built. An explicit `canonical` resolves them from the source alone.
|
|
110
|
+
|
|
111
|
+
Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
|
|
112
|
+
thing (a `bulk.update` zero-filling omitted columns, an `ignoreEmpty` on an empty
|
|
113
|
+
operand, a `ref()` nothing binds). Each has a legitimate use, so each only warns —
|
|
114
|
+
but nothing fails on a message no one reads, so in CI and in unattended agent builds
|
|
115
|
+
pass `--strict` (`emitBundle(app, { strict: true })` / `app.export({ strict: true })`):
|
|
116
|
+
every warning becomes a hard failure. Same bundle bytes either way.
|
|
117
|
+
|
|
118
|
+
Identity: object guids derive from `(type, name)`, so renames change identity.
|
|
119
|
+
`xanots export <entry> --lock` freezes every guid + api-group/toolset canonical in a lock
|
|
120
|
+
file written BESIDE THE ENTRY FILE — `xano/xano.lock` for the standard scaffold, NOT the
|
|
121
|
+
project root — which you commit (auto-read once present; CI guard `--frozen-lock` fails
|
|
122
|
+
instead of changing the lock). Fix-up subcommands:
|
|
123
|
+
`xanots lock rename <kind> <old> <new>` (kind = payloadKey or table/api_group),
|
|
124
|
+
`xanots lock prune <entry-file> [keys…] --yes` — finding orphans RUNS the entry's module scope
|
|
125
|
+
(env assertions included); `--no-verify --yes <kind:name>…` prunes named keys with no evaluation
|
|
126
|
+
and no orphan check,
|
|
127
|
+
`xanots lock adopt <live-bundle.json> [--yes]` — all accept `--lock=<path>`.
|
|
128
|
+
Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing
|
|
129
|
+
any def module — references bake guids at import time, so late seeding is a
|
|
130
|
+
silent no-op (`resetLockOverrides` exists for tests).
|
|
131
|
+
Development workflow: opt in EARLY — run `xanots export ./xano/index.ts --lock` once and COMMIT
|
|
132
|
+
`xano.lock` beside the entry file; every later export then keeps identities
|
|
133
|
+
stable across renames and environments. To rename an object: rename in code,
|
|
134
|
+
export (stderr prints the exact fix-up), run `xanots lock rename <kind> <old>
|
|
135
|
+
<new>`, export again — the original guid is emitted under the new name, so the
|
|
136
|
+
engine renames in place instead of delete+create. Taking over an existing
|
|
137
|
+
workspace: `xanots lock adopt <its-packageExport.json>` first, then export.
|
|
138
|
+
|
|
139
|
+
## Deploy
|
|
140
|
+
|
|
141
|
+
`xanots init` → `xanots deploy` → URL. Each `deploy` runs the same compile
|
|
142
|
+
pipeline as `export` (honoring `xano.lock`), then imports the result into a live
|
|
143
|
+
environment and prints its URL.
|
|
144
|
+
⚠ Every deploy is a FULL REPLACE: it clears that environment's workspace — objects
|
|
145
|
+
AND records — before importing. The blast radius is a disposable environment, not a
|
|
146
|
+
production workspace, but confirm with the user before the first run.
|
|
147
|
+
|
|
148
|
+
**Two destinations, and the choice changes more than the target.**
|
|
149
|
+
|
|
150
|
+
- `--dest ephemeral` (DEFAULT) — a NAMED, workspace-scoped, auto-expiring tenant
|
|
151
|
+
(~1h; `--expires-hours` 1–72 at create time). The active one is tracked in
|
|
152
|
+
`./.xano/ephemeral.json`, so deploying again REFRESHES it and the URL is unchanged;
|
|
153
|
+
if it expired or was swept, a fresh one is created and the new URL is called out.
|
|
154
|
+
`--static` puts the frontend ON THE EPHEMERAL, so backend and frontend share one
|
|
155
|
+
disposable environment.
|
|
156
|
+
⚠ Only the BACKEND URL survives a refresh: the replace clears static hosting too,
|
|
157
|
+
so `--static` publishes a NEW host every run and the previous URL stops serving.
|
|
158
|
+
- `--dest sandbox` — your single throwaway tenant, no expiry. `--static` puts the
|
|
159
|
+
frontend on your OWN (parent) workspace instead, because the sandbox tenant does
|
|
160
|
+
not serve static hosting.
|
|
161
|
+
- `xanots release` promotes to your INSTANCE workspace and MERGES, not replaces:
|
|
162
|
+
adds/updates what you define, deletes nothing, writes no rows. Destruction is
|
|
163
|
+
opt-in per flag, previewed + confirmed, and can drop a table WITH its rows.
|
|
164
|
+
NOT flag-gated: removing a column from a schema destroys that column's data
|
|
165
|
+
(previewed against live). `--prune` deletes only what `xano.lock` records this
|
|
166
|
+
project released, and refuses without a lock.
|
|
167
|
+
|
|
168
|
+
Nothing from a DEPLOY is written back into `xano.lock` (an ephemeral/sandbox is a
|
|
169
|
+
separate workspace, so its identities must not pollute yours). The one write-back is
|
|
170
|
+
`release --replace`, which mints fresh identities in the workspace the lock describes:
|
|
171
|
+
it re-pins the lock from the rebuilt workspace, because otherwise the next release
|
|
172
|
+
matches nothing and duplicates every object. Deploying an ENTRY
|
|
173
|
+
FILE still updates the local lock via the shared compile step, exactly as `export`
|
|
174
|
+
does — only when a lock exists or `--lock` is passed.
|
|
175
|
+
|
|
176
|
+
**Frontend wiring.** `--static <dir>` injects the DEPLOYED env's backend URL as
|
|
177
|
+
`window.XANO_HOST` into EVERY html document in the build, before the app bundle runs,
|
|
178
|
+
so the frontend needs no rebuild to target an env. Every document, not just the root:
|
|
179
|
+
a prerendered build serves a different one per route, and a route without the global
|
|
180
|
+
renders fine while every call goes to the wrong origin. Read it at runtime with a build-time
|
|
181
|
+
fallback:
|
|
182
|
+
const HOST = (typeof window !== 'undefined' && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
|
|
183
|
+
In LOCAL DEV there is no injected global, so the fallback is what answers: set
|
|
184
|
+
`VITE_XANO_HOST` in a `.env.local` beside `.env.example` at the PROJECT ROOT. The
|
|
185
|
+
scaffold's vite config sets `envDir` there (its `root` is `frontend/`, and Vite
|
|
186
|
+
resolves `.env` files against `root`) — without it the var reads as undefined, the
|
|
187
|
+
host falls back to '', and every call 404s off the dev server.
|
|
188
|
+
⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
|
|
189
|
+
by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
|
|
190
|
+
bare `XANO_HOST` token.
|
|
191
|
+
⚠ `xanots validate` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
192
|
+
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
193
|
+
**Displaying a stored file.** A file column comes back as `{ path, name, type, size,
|
|
194
|
+
meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
|
|
195
|
+
addresses the instance host WITHOUT the `/tenant/<name>` segment and 404s, silently —
|
|
196
|
+
as a broken `<img>`, with every API assertion still passing. Build the URL from `path`
|
|
197
|
+
instead: `fileUrl(row.avatar, HOST)` (exported from `@xanots/sdk`) returns `null` for
|
|
198
|
+
an absent file and is correct on an ephemeral and an instance workspace alike.
|
|
199
|
+
⚠ A static host serves these files verbatim, so everything injected is PUBLIC — base
|
|
200
|
+
URLs and publishable keys only, never secrets. Secrets go in backend env, read
|
|
201
|
+
server-side via `env(name)`.
|
|
202
|
+
|
|
203
|
+
**Full CLI surface:** `xanots <command> --help` lists every command, flag, and
|
|
204
|
+
default; the shipped `manifest.json` carries the same in its `cli` array. This doc
|
|
205
|
+
does not duplicate it — it covers what you must know to AUTHOR a workspace.
|
|
206
|
+
**Recommended style:** reach statements through the `s` namespace
|
|
207
|
+
(`s.db.add`, `s.math.add`, …) — one discoverable, tab-completable surface. The
|
|
208
|
+
flat factory aliases (`dbAdd`, `dbQuery`, `setVar`, `mathAdd`, …) are exported
|
|
209
|
+
and identical in output; prefer `s.*` in new code so examples stay consistent.
|
|
210
|
+
|
|
211
|
+
## Gotchas
|
|
212
|
+
|
|
213
|
+
Non-obvious authoring rules:
|
|
214
|
+
|
|
215
|
+
- **No callback builder.** Flat def-objects + `register*`, not
|
|
216
|
+
`workspace(w => w.table(...))`. `workspace(name)` returns a named `new Xano()`;
|
|
217
|
+
tables are `table({ schema: { col: f.text() } })`.
|
|
218
|
+
- **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a
|
|
219
|
+
stack variable (a value); `f.tableRef` is the column constructor.
|
|
220
|
+
- **Reference-helper picker:** `ref` = stack var (`as:` output), `inp` = input,
|
|
221
|
+
`col` = table column (in `db.query` `where`), `auth("id")` = the caller,
|
|
222
|
+
`c.*` = a constant. Pick by what you're pointing at.
|
|
223
|
+
- **`s.api.call` / `s.task.call` / `s.trigger.call` / `s.workflow_test.call` are
|
|
224
|
+
WORKFLOW-TEST ONLY.** Outside a `workflowTest({...})` stack the engine cannot reach
|
|
225
|
+
the target, so one in a query/function/task deploys clean and then answers the first
|
|
226
|
+
real request with `ERROR_FATAL: <Type> does not exist: <type>:<n>` — and not per
|
|
227
|
+
host kind, the same call fails identically from a function a query runs.
|
|
228
|
+
`export()` refuses them. `s.function.call`, `s.tool.call`, `s.middleware.call` and
|
|
229
|
+
`s.addon.call` run anywhere, as does `s.function.run` (the ordinary function
|
|
230
|
+
invocation). To share logic between endpoints, put it in a `defineFunction` and
|
|
231
|
+
`s.function.run` it from both.
|
|
232
|
+
- **`expect.*` is a unit-test assertion; `s.expect.*` is a workflow-test statement.**
|
|
233
|
+
A statement's `mock` is keyed by TEST NAME; an undeclared name throws. See `llms/tests.md`.
|
|
234
|
+
- **Drilling into a maybe-null `db.get` result 500s — use `ref(path, { safe: true })`.**
|
|
235
|
+
`db.get` binds `null` on a no-match, but a nested `ref("owner.user_id")` resolves
|
|
236
|
+
`$owner.user_id` in one lookup and raises a runtime "Unable to locate var" (HTTP 500)
|
|
237
|
+
when `owner` is null — so an ownership/existence guard throws instead of failing
|
|
238
|
+
cleanly. Either guard existence first (`expr(ref("owner"), "!=", c.null())`,
|
|
239
|
+
or a `db.has`/`db.query`-count precondition), or drill with the null-safe opt-in:
|
|
240
|
+
`expr(ref("owner.user_id", { safe: true }), "=", auth("id"))` compiles through the
|
|
241
|
+
`get` filter and yields `null` (guard reads `false`) rather than 500ing.
|
|
242
|
+
Works inside `obj({...})` too — no per-member `s.set_var` hoist needed.
|
|
243
|
+
⚠ EXPRESSION and `obj()` operands ONLY — never a `db.*` match argument. `null`
|
|
244
|
+
is not a legal `fieldValue`/`id`, so a chained get (fetch the child row, then
|
|
245
|
+
fetch its parent to check the owner) fails with HTTP 400 `Missing param:
|
|
246
|
+
field_value` one statement BEFORE the guard — the same net effect as the 500,
|
|
247
|
+
with an internal-sounding status the caller should never see. Only an id
|
|
248
|
+
matching no row reaches it, so it deploys clean. In the chained case guarding
|
|
249
|
+
existence first is MANDATORY, not a stylistic alternative: precondition on
|
|
250
|
+
`expr(ref("ticket"), "!=", c.null())`, then drill WITHOUT `safe` (the base is
|
|
251
|
+
known non-null by then). `export()` warns on a safe ref in that position.
|
|
252
|
+
⚠ That is for a base that EXISTS and may be null. If the base binds nothing at all
|
|
253
|
+
(a typo), `{ safe: true }` turns the 500 into a silent `null` and buries the bug —
|
|
254
|
+
`export()` warns on a `ref()` whose base segment no `as` in that stack binds, with a
|
|
255
|
+
did-you-mean; fix the name instead.
|
|
256
|
+
- **DB reads are field-match, not `where`-expr.** `db.get`/`db.edit`/`db.del`/
|
|
257
|
+
`db.has`/`db.patch` match one field: `{ fieldName, fieldValue }` (`fieldName`
|
|
258
|
+
defaults to the PK `id`). Only `db.query` takes a `where`/`additionalWhere`
|
|
259
|
+
`expr(...)`; writes (`db.add`/`db.edit`/`db.add_or_edit`) take a `row`/`data`.
|
|
260
|
+
`db.get_by_id` is the exception to the field-match rule — it is the dedicated
|
|
261
|
+
get-by-primary-key statement and takes `{ table, id }`, NOT `fieldValue`.
|
|
262
|
+
Signatures: `llms/statements-data.md`.
|
|
263
|
+
- **Single-field only — no composite match.** These ops match exactly ONE field;
|
|
264
|
+
there is no two-field form (the engine's by-field lookup takes a single
|
|
265
|
+
predicate). For a `(a, b)` existence/fetch — e.g. dedupe a `(habit, date)`
|
|
266
|
+
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
|
+
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
|
+
check to the client.
|
|
269
|
+
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
|
+
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
|
+
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
272
|
+
Both are valid targets wherever a column name is accepted — `db.query` `sort`/
|
|
273
|
+
`output`, a `db.get`/`edit`/`del` `fieldName`, etc. (the column-name type is
|
|
274
|
+
`keyof schema | "id" | "created_at"`), and both appear in `InferRow<typeof table>`.
|
|
275
|
+
- **Seed a table's starting rows with `table({ seed })`.** `seed` takes rows
|
|
276
|
+
typed against the table's schema as a WRITE shape (a column without
|
|
277
|
+
`required: true`, and the system columns, may be omitted; `null` needs
|
|
278
|
+
`nullable: true`) — inline (`seed: [{ name: "…" }]`), a FILE
|
|
279
|
+
(`seed: seedFile("./seed.json", import.meta.url)`; path resolves against the DECLARING
|
|
280
|
+
file), or a thunk (`seed: () => import("./seed.json")`, async ok, `.default` unwrapped).
|
|
281
|
+
Inline rows are TYPED against the schema at compile time; a `seedFile`/thunk seed is
|
|
282
|
+
NOT — the JSON is opaque to the typechecker, and the `xanots export`/`deploy` CLI
|
|
283
|
+
validates it instead, naming the row index, the offending column, and the known
|
|
284
|
+
columns. ⚠ Prefer `seedFile` for a file: a thunk's `import()` sits in
|
|
285
|
+
YOUR module, so a bundler emits the JSON as a served chunk. NEITHER is a secrecy
|
|
286
|
+
boundary — a bundler that inlines the asset ships them either way. Never put secrets
|
|
287
|
+
in `seed`. `deploy --static` REFUSES a build carrying internal/sensitive seed values;
|
|
288
|
+
`--allow-seed-in-static` overrides for deliberately public demo credentials.
|
|
289
|
+
Deploy is a full replace, so re-deploying re-seeds
|
|
290
|
+
cleanly (no duplication). Omit `id` and rows auto-number `1..N` (int PK) or take
|
|
291
|
+
a stable derived uuid (uuid PK); supplying `id` pins it (engine preserves it,
|
|
292
|
+
resets an int sequence past the max). All-or-nothing — mixing explicit and
|
|
293
|
+
omitted `id` throws. A `system:false` PK is the author's to supply. Pinning is
|
|
294
|
+
`seed`-only — `s.db.bulk.add` DROPS `id` unless `allowIdField: true`.
|
|
295
|
+
- **`use_xdo` storage mode.** Workspace setting (`registerWorkspace({ use_xdo })`,
|
|
296
|
+
default `false`) controlling whether fields are stored as JSON under the `xdo`
|
|
297
|
+
column (`true`, adds a `gin(xdo)` index) or as real columns (`false`, no gin).
|
|
298
|
+
Tables inherit it; override per-table with `table({ useXdo })`. Resolved at
|
|
299
|
+
`export()`, so the workspace and tables can be registered in any order.
|
|
300
|
+
- **Self-referencing tables** need the bare-name form: inside `tweets`'s own
|
|
301
|
+
schema, write `f.tableRef("tweets", { type: "int" })` — the `const tweets`
|
|
302
|
+
handle isn't assigned yet, so the handle form throws "used before declaration".
|
|
303
|
+
- **Same-name siblings collide.** Object guids derive from `(type, name)` and
|
|
304
|
+
ignore a query's `verb`, so a `GET` and `POST` both named `posts` clash;
|
|
305
|
+
`export()` throws on the collision — give them DISTINCT names (`posts` and
|
|
306
|
+
`posts/create`; the verb still carries the semantics). A distinct explicit
|
|
307
|
+
`guid` on each clears that throw but is NOT a lasting fix: a lock entry is
|
|
308
|
+
keyed by `(type, name)` too and cannot hold two guids, so `export --lock`
|
|
309
|
+
refuses the pair — and a committed lock is the recommended workflow.
|
|
310
|
+
`export()` warns on a same-name pair even unlocked. An explicit `guid` is
|
|
311
|
+
for pinning identity across a rename, not for sharing a name.
|
|
312
|
+
- **`export()` vs `emitBundle()` vs `writeBundle()`:** `writeBundle(app, path)`
|
|
313
|
+
writes it to disk; `export()` returns the bundle object;
|
|
314
|
+
`emitBundle()` returns the pretty JSON string. All three run the SAME build-time
|
|
315
|
+
checks, INCLUDING seed validation of a literal `seed: [...]` array (row shape,
|
|
316
|
+
unknown column, coercion, enum membership, the all-or-nothing `id` rule). A
|
|
317
|
+
DEFERRED seed — a thunk (`() => import("./seed.json")`) or `seedFile()` — needs an
|
|
318
|
+
await or the filesystem, so it is materialised and checked only by the
|
|
319
|
+
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
320
|
+
either way; only `deploy` ships them. The `node:fs` writers
|
|
321
|
+
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
322
|
+
NOT the browser-safe `@xanots/sdk` entry (which a frontend can import query
|
|
323
|
+
defs from to use `getPath()`/`InferInput` with no Node built-ins in the bundle).
|
|
324
|
+
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
325
|
+
the lock model) is on `@xanots/sdk/internal` and is never needed to author.
|
|
326
|
+
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
327
|
+
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
328
|
+
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
329
|
+
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
330
|
+
⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
|
|
331
|
+
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
332
|
+
one, adds ~1 kB. So the cost is paid by importing ANY def at all, and reducing what a
|
|
333
|
+
def does will not reduce it.
|
|
334
|
+
Fix: `xanots paths <entry> --emit xano/routes.gen.ts` (`routes` is an accepted alias) — verbs, paths, and sockets as
|
|
335
|
+
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
336
|
+
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
337
|
+
URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
|
|
338
|
+
- **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
|
|
339
|
+
extensionless — the defs compile under `moduleResolution: bundler`. Add the `.js`.
|
|
340
|
+
- **Verifying a def outside a bundler.** Inside a bundler (Vite/webpack) importing a
|
|
341
|
+
query def to read `getPath()`/`verb` works directly. To spot-check from Node, run a REAL
|
|
342
|
+
file with `tsx <file.ts>` **from inside the project root** — not `tsx -e "import …"`
|
|
343
|
+
(its CJS-preparse mis-resolves the package `exports` map → ERR_PACKAGE_PATH_NOT_EXPORTED),
|
|
344
|
+
and not bare `node file.ts` (chokes on the `.js`-specifier intra-workspace imports the
|
|
345
|
+
xanots CLI's own loader resolves). Running from outside the project root also breaks
|
|
346
|
+
the `@xanots/sdk` specifier resolution.
|
|
347
|
+
- **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**
|
|
348
|
+
Factoring a repeated guard into `function requireX(): Statement[]` and spreading it
|
|
349
|
+
(`...requireX("d")`) drops the stack's TUPLE type, so EVERY `ref()`/`as` in that stack —
|
|
350
|
+
including ones declared after the spread — resolves to `unknown` and the query's
|
|
351
|
+
response types as `StackTupleWidened`. Nothing fails at the helper: the error surfaces
|
|
352
|
+
wherever the response is consumed, often a frontend package's typecheck. Fix: return
|
|
353
|
+
`statements(s.a(...), s.b(...))` — a const-generic identity export whose tuple survives
|
|
354
|
+
the spread. A helper that builds its array in a LOOP cannot be a tuple; declare
|
|
355
|
+
`responseShape` on the query there. See `llms/statements-runtime.md`.
|
|
356
|
+
- **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/
|
|
357
|
+
`s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
|
|
358
|
+
take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
|
|
359
|
+
`s.group(body)` and `s.util.post_process(body)` take it **positionally**.
|
|
360
|
+
`s.for` is **count-bounded** (`{ as, count, body }`), not from/to. See the
|
|
361
|
+
authored signatures in `llms/statements-data.md`.
|
|
362
|
+
- **MCP servers & agents are distinct root kinds** that both persist under the
|
|
363
|
+
`toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes
|
|
364
|
+
tools over MCP (auth is per-tool — no server-level gate); `agent({...})` carries a
|
|
365
|
+
typed `llm` block — and so may an `mcpServer`, since the two are ONE stored
|
|
366
|
+
object distinguished by `type`. Their `tools` take a `tool()` handle (or name), resolved to the
|
|
367
|
+
tool's guid like the call family; a raw numeric `id` is an escape hatch.
|
|
368
|
+
- **`task.schedule` is an array** of `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`
|
|
369
|
+
(`ScheduleDef[]`), not a single `{ type, value }`. `freq` is seconds; `startsOn`/
|
|
370
|
+
`endsOn` are timestamp strings validated at encode time — `"2026-01-01T00:00:00Z"`, or
|
|
371
|
+
the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries. Never
|
|
372
|
+
epoch numbers, and never zoneless: the engine stores the string verbatim without
|
|
373
|
+
checking it, so one it cannot parse deploys clean and the task then never fires.
|
|
374
|
+
- **`get_input`/`get_raw_input` read the whole payload**, not one named input
|
|
375
|
+
(args are `{ as?, encoding?, excludeMiddleware? }` — no `name`). For a single
|
|
376
|
+
input use `inp("name")`.
|
|
377
|
+
- **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**
|
|
378
|
+
A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime
|
|
379
|
+
with `Unable to locate var: u.password` — the column is simply absent from the row.
|
|
380
|
+
Name it in the read's `output` to pull it: `s.db.get({ table: users, fieldName: "email",
|
|
381
|
+
fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then
|
|
382
|
+
`s.security.check_password`. `output` OVERRIDES column visibility — it is the only way to
|
|
383
|
+
read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not
|
|
384
|
+
return.
|
|
385
|
+
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
386
|
+
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
387
|
+
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped. A bare
|
|
388
|
+
`c.text("^[^@\s]+@...$")` is an invalid pattern that matches *nothing* for every
|
|
389
|
+
input, so a precondition on it silently rejects all values (valid ones included).
|
|
390
|
+
`c.regex("^[^@\s]+@[^@\s]+\.[^@\s]+$", "i")` wraps + escapes it for you (accepts a
|
|
391
|
+
JS `RegExp` too: `c.regex(/^…$/i)`), and `withFilters` throws on a bare `c.text`
|
|
392
|
+
pattern pointing here — from ANY position in the chain, so a normalizer in front
|
|
393
|
+
(`withFilters(c.text("^hi"), fl.trim(), fl.regex_test(...))`) is refused too; nothing
|
|
394
|
+
upstream adds the delimiters. `s.expect.to_match`'s `value` is the same PATTERN slot,
|
|
395
|
+
refused the same way; a `ref`/`inp` pattern is passed through untouched, either place.
|
|
396
|
+
Better still, prefer a native typed input
|
|
397
|
+
(`input.email({...})`) over hand-rolled validation.
|
|
398
|
+
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
399
|
+
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
400
|
+
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
|
401
|
+
folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.
|
|
402
|
+
- **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`
|
|
403
|
+
resolves ONLY against this def's own `input` block — a value produced earlier in
|
|
404
|
+
the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at
|
|
405
|
+
runtime with `Unable to locate input` (`export()` warns).
|
|
406
|
+
- **Don't take a password through `input.password` on login — it double-hashes.**
|
|
407
|
+
An `f.password()` column hashes on write, and `input.password` *also* hashes the
|
|
408
|
+
submission on bind, so `s.security.check_password` compares two different hashes
|
|
409
|
+
and a correct password always fails (`ok:false` on a found row). Take the submitted
|
|
410
|
+
password as `input.text()` on both signup and login and pass the plaintext straight
|
|
411
|
+
to `check_password` (which does the comparison hash itself).
|
|
412
|
+
- **Agents authenticate with env vars — never `xanots login`.** `login` blocks on a
|
|
413
|
+
browser consent no agent can complete. Set `$XANO_INSTANCE_URL` + `$XANO_WORKSPACE_ID`
|
|
414
|
+
+ `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no rotation, so it
|
|
415
|
+
survives repeated runs. It outranks every other credential (`--config` and
|
|
416
|
+
`$XANO_REFRESH_TOKEN` included, each named on stderr when displaced), and setting SOME
|
|
417
|
+
of the three is a hard error — a misspelled secret must not fall through to whatever
|
|
418
|
+
credential the runner has. As a file that triple is `{ "type": "token",
|
|
419
|
+
"instance_base_url": …, "workspace_id": <n>, "meta_api_token": … }`. The older
|
|
420
|
+
`$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works but ROTATES: single-use.
|
|
421
|
+
- **Event-driven objects fire on an EPHEMERAL, not in the sandbox.** A `task`
|
|
422
|
+
(scheduled), an `mcpServer`, and every trigger — `tableTrigger` included — run normally
|
|
423
|
+
on an ephemeral env, which is `deploy`'s DEFAULT destination. So test an event-driven
|
|
424
|
+
design (screen-on-insert, cron cleanup, MCP tool call) by deploying it and letting it
|
|
425
|
+
run.
|
|
426
|
+
⚠ Under `--dest sandbox` they import cleanly but their stacks NEVER execute, and there
|
|
427
|
+
is no way to fire one manually — an insert on a bound table does not run its
|
|
428
|
+
`tableTrigger`, and the design silently does nothing. Only synchronously-invoked objects
|
|
429
|
+
(queries, functions, and the agents an endpoint calls with `s.ai.agent.run`) run there.
|
|
430
|
+
If you must stay on the sandbox, verify the logic out of band: factor the body into a
|
|
431
|
+
`defineFunction` (or a callable `query`) and invoke it directly — a `tableTrigger` that
|
|
432
|
+
screens a row on insert should delegate to a function a `query` can also call via
|
|
433
|
+
`s.function.run`, and you assert against that.
|
|
434
|
+
- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
|
|
435
|
+
`["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
|
|
436
|
+
`0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
|
|
437
|
+
want the list, or prefix the keys (`"k0"`) when you want an object. A non-zero-based
|
|
438
|
+
numeric key (`{ "2": … }`) survives as a key. Export warns.
|
|
439
|
+
- **To match every row, omit `where` — never pass a constant.** `where: c.bool(true)` is
|
|
440
|
+
not "no filter": a bare scalar literal states no condition and the engine reads the
|
|
441
|
+
resulting `context.search` as garbage, so it throws. An absent `where` is how "every
|
|
442
|
+
row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built
|
|
443
|
+
elsewhere — `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.
|
|
444
|
+
- **An `auth()`-keyed limiter collapses on any host with no caller identity — at every**
|
|
445
|
+
**tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))) })`
|
|
446
|
+
is the per-user form, but `auth()` is `null` on a public query, a task, or a function,
|
|
447
|
+
and every caller then shares one bucket. Attaching it once at
|
|
448
|
+
`apiGroup({ middleware })` or the workspace tier is the tempting DRY move and inherits
|
|
449
|
+
the same collapse onto every member endpoint. Export warns, naming the tier.
|
|
450
|
+
|
|
451
|
+
## Statements
|
|
452
|
+
|
|
453
|
+
Reachable through the `s` namespace: `s.<path>({...})`. Declarative statements
|
|
454
|
+
take one typed args object (field names match the engine); every surface's field
|
|
455
|
+
signature is in `llms/statements-catalog.md`. Specials (`[special]`) are
|
|
456
|
+
hand-authored: control flow is below, and the rest are in the `llms/statements-*`
|
|
457
|
+
topic files this doc's navigation names.
|
|
458
|
+
Wrap an `input`-routed `value` field in `ignored(...)` to store it but SKIP it at
|
|
459
|
+
runtime — the engine records `<name>:ignore` and the parameter falls back to its
|
|
460
|
+
default. Not the same as an empty value, and not the same as omitting the field
|
|
461
|
+
(which stores no entry at all). Mostly seen on a pulled workspace. A field written
|
|
462
|
+
into `context` instead (the `s.math.*`/`s.text.append` mutation family) has no slot
|
|
463
|
+
for the flag and THROWS rather than dropping it.
|
|
464
|
+
Fields marked `value` take a `Value` (`c.*`/`ref`/`inp`); `comparison` takes an
|
|
465
|
+
`expr(...)`. A `→ as: <type>` suffix names what the statement's `as:` output var
|
|
466
|
+
holds (curated, not exhaustive — absence means read the `[output]` flag and prose).
|
|
467
|
+
|
|
468
|
+
### Specials — authored signatures
|
|
469
|
+
|
|
470
|
+
Control flow & blocks (each nests a sub-stack; block specials name it `body`):
|
|
471
|
+
|
|
472
|
+
- `s.set_var(name, value)` · `s.update_var(name, value)` · `s.return(value)` · `s.comment(text)` — positional.
|
|
473
|
+
- **Every** statement takes `disabled?`/`description?` — annotations on the stack item, not args: `disabled: true` is Xano's "disable step" (kept in the stack, skipped at runtime), `description` the note beside it. Inline on object-arg factories; a trailing object on the positional ones (`s.set_var("x", v, { disabled: true })`).
|
|
474
|
+
- **Statements with an `as`** also take `asFilters?` — `fl.*` filters on the RESULT as it binds, in order, same slot as `disabled`: `s.set_var("x", v, { asFilters: [fl.trim(), fl.lower()] })`. Saves a follow-up `set_var`. Throws without an `as`. The bound variable is RETYPED by the chain (`db.query` + `[fl.count()]` → `number`); filters whose result the engine declares as `any` (`get`, `set`, `json_decode`, …) fold to `unknown`.
|
|
475
|
+
- `s.conditional({ when, then, elif?, else? })` — if/elif/else. `when` is a condition (`expr`/`cmp`/`and`/`or`); `elif` is an ordered `[{ when, then }]` (each an else-if branch); `then`/`else` are `Statement[]`.
|
|
476
|
+
- `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to.
|
|
477
|
+
- `s.foreach({ as, list, body })` — iterate `list`; `as` is the current item.
|
|
478
|
+
- `s.while({ when, body })` — `when` is a condition (`expr`/`cmp`/`and`/`or`).
|
|
479
|
+
- `s.switch({ on, cases: [{ when, body, break? }], default? })` — multi-way branch on a subject `Value` `on`; each `case`'s `when` is a literal `Value` matched against `on` (NOT a comparison — use `s.conditional` for `<`/`>`/ranges). ⚠ **Omitting `break: true` FALLS THROUGH** — the matched case also runs every LATER case body. Type-checks clean; only `export --strict` catches it.
|
|
480
|
+
- `s.try_catch({ try, catch?, finally? })` — three `Statement[]` blocks.
|
|
481
|
+
- `s.group(body)` / `s.util.post_process(body)` — take a `Statement[]` **positionally**.
|
|
482
|
+
- `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` — nullary loop control.
|
|
483
|
+
- `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise.
|