@xanots/sdk 0.0.8 → 0.0.10
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/CHANGELOG.md +29 -0
- package/README.md +28 -21
- package/dist/.build-fingerprint +1 -1
- package/dist/{agent-file-refresh-4W37DGXS.js → agent-file-refresh-QNKN5RYD.js} +3 -3
- package/dist/bin.js +8 -7
- package/dist/{branch-commands-PBQ36L4A.js → branch-commands-2BLOC2GR.js} +13 -11
- package/dist/{capture-7PO6SGB4.js → capture-4WVJY4DQ.js} +2 -2
- package/dist/{chunk-ZOYMZZ3S.js → chunk-3INK4Y4E.js} +1 -1
- package/dist/{chunk-NDZFBZHC.js → chunk-3UABJA4X.js} +1 -1
- package/dist/{chunk-7AXQKABT.js → chunk-5R73LFWK.js} +3 -3
- package/dist/{chunk-CDAQUY5W.js → chunk-5YCQ2QHH.js} +2 -2
- package/dist/{chunk-2ROR3AZC.js → chunk-75Z74TA7.js} +2 -2
- package/dist/{chunk-QTNO2WD6.js → chunk-7ZYW652H.js} +1 -1
- package/dist/{chunk-SNO3KHCY.js → chunk-ACCBOMCB.js} +109 -78
- package/dist/{chunk-4PTSTDIG.js → chunk-AIZKXUNP.js} +2 -2
- package/dist/{chunk-P5C2YKAM.js → chunk-ANUDXFEX.js} +18 -27
- package/dist/chunk-CTD5ZCV6.js +28 -0
- package/dist/{chunk-5HCDZ2XN.js → chunk-DBFU47BJ.js} +2 -2
- package/dist/{chunk-OKNSR7MT.js → chunk-DIA7CT7J.js} +129 -150
- package/dist/{chunk-JQLR64UC.js → chunk-F6CYJ7TN.js} +21 -6
- package/dist/{chunk-BKDJN76G.js → chunk-FE5I6S6N.js} +1 -1
- package/dist/{chunk-EM5QPTXP.js → chunk-I7DQDJAM.js} +30 -48
- package/dist/{chunk-QIWBCS7N.js → chunk-JGCWTCA7.js} +2 -2
- package/dist/{chunk-MDXR5E5Q.js → chunk-K5IOND4K.js} +2 -2
- package/dist/{chunk-4PE4ZAB7.js → chunk-KA6G2L7U.js} +5 -5
- package/dist/{chunk-64QK6JEK.js → chunk-LBYWGMOA.js} +36 -2
- package/dist/{chunk-3MDKV2VA.js → chunk-P6TAVLOX.js} +2 -2
- package/dist/{chunk-3QW5NFZJ.js → chunk-QKM4U5UK.js} +3 -3
- package/dist/{chunk-T4XPCJRF.js → chunk-TCFIPDB3.js} +1 -1
- package/dist/{chunk-FEQTQ6PM.js → chunk-VKSOTZK3.js} +2 -2
- package/dist/{chunk-6DHBYBTO.js → chunk-VNQM3V2C.js} +1 -2
- package/dist/{chunk-PYS7UNNW.js → chunk-W24FJHPD.js} +3 -3
- package/dist/chunk-WGPXT2G2.js +22 -0
- package/dist/{chunk-6AAT2AYQ.js → chunk-WP4OZZV4.js} +2 -2
- package/dist/{chunk-MU3O43L2.js → chunk-YBC3IKMF.js} +2 -2
- package/dist/{chunk-C3M5K4ZH.js → chunk-YYFXVYPX.js} +37 -6
- package/dist/chunk-ZQ2PKR6R.js +40 -0
- package/dist/cli.d.ts +57 -31
- package/dist/cli.js +6 -5
- package/dist/codegen-command-GB3H2KQ7.js +46 -0
- package/dist/{completion-A6BZ3XGU.js → completion-WF46272M.js} +2 -2
- package/dist/{config-NL33PN4D.js → config-476F3PT5.js} +1 -1
- package/dist/{deploy-command-QZ3GNLAD.js → deploy-command-QTIVA22A.js} +68 -100
- package/dist/{ephemeral-command-DL7EY4LL.js → ephemeral-command-U4AQ3TXX.js} +25 -28
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/{init-command-4MG3UKUO.js → init-command-MAXULNAD.js} +13 -10
- package/dist/{onboard-command-EHOHKQQU.js → init-web-3JNFG6GI.js} +10 -10
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +42 -45
- package/dist/{live-diff-QK4KC2FJ.js → live-diff-IXKBVG4K.js} +3 -3
- package/dist/{lock-commands-Y4S4NUCE.js → lock-commands-DQ7CUMIV.js} +20 -19
- package/dist/{login-command-DFCURZLC.js → login-command-Z6CHTA57.js} +7 -7
- package/dist/{logout-command-WXVZQCAV.js → logout-command-ER6IKAYJ.js} +2 -2
- package/dist/{loop-7SAIGRCZ.js → loop-D5NPL4VH.js} +3 -3
- package/dist/{marketplace-command-BCOAAN2I.js → marketplace-command-P4IPLJ6J.js} +6 -6
- package/dist/{meta-client-57ZWVHST.js → meta-client-K2J4XH64.js} +6 -6
- package/dist/node.d.ts +2 -2
- package/dist/node.js +9 -8
- package/dist/{validate-command-GLF5ZOOW.js → preflight-command-GZ2GE5RN.js} +17 -16
- package/dist/{profile-command-QJAAUZIV.js → profile-command-LJSBDV2L.js} +5 -5
- package/dist/{release-command-DN75G5GJ.js → release-command-YUBTNHVX.js} +36 -33
- package/dist/{routes-manifest-SP3ZXLMR.js → routes-manifest-PWZHDOI5.js} +2 -2
- package/dist/scaffold.js +2 -2
- package/dist/{static-host-D6KS7X45.js → static-host-3WMV7IZO.js} +1 -1
- package/dist/status-command-AL47VG7H.js +162 -0
- package/dist/{store-CUCBSYLj.d.ts → store-BG1UPZ3Z.d.ts} +5 -5
- package/dist/{test-command-RRNNJ45I.js → test-command-YAZLKLGQ.js} +46 -29
- package/dist/{upgrade-command-XFOAWDI3.js → upgrade-command-FB5QJ363.js} +13 -12
- package/dist/{workspace-K72NP7SX.js → workspace-2COHDBM3.js} +1 -1
- package/dist/{workspace-command-QCFELEGR.js → workspace-command-43P42FBP.js} +31 -32
- package/dist/{workspace-export-AJMGN3CQ.js → workspace-export-DURY5WYL.js} +2 -2
- package/guides/README.md +2 -2
- package/guides/authoring.md +9 -1
- package/guides/cli.md +17 -19
- package/guides/codegen.md +21 -5
- package/guides/coverage.md +1 -1
- package/guides/deploying.md +31 -34
- package/guides/environment.md +4 -4
- package/guides/object-kinds.md +3 -3
- package/guides/project-structure.md +1 -1
- package/guides/scaffold.md +25 -2
- package/guides/typed-frontend.md +1 -1
- package/llms/fields.md +3 -2
- package/llms/kinds-agent-mcp.md +1 -1
- package/llms/kinds-core.md +1 -1
- package/llms/kinds-realtime.md +1 -1
- package/llms/lock.md +5 -5
- package/llms/statements-calls.md +2 -1
- package/llms/statements-data.md +5 -2
- package/llms/tests.md +3 -2
- package/llms-full.txt +41 -44
- package/llms.txt +20 -29
- package/manifest.json +95 -426
- package/package.json +1 -1
- package/dist/chunk-3EYUR3TX.js +0 -100
- package/dist/chunk-JQJPFUZI.js +0 -118
- package/dist/chunk-XP7S3VWY.js +0 -78
- package/dist/codegen-command-U2W72DLG.js +0 -43
- package/dist/env-target-NAYVEXBH.js +0 -16
- package/dist/sandbox-details-command-EDO56IY3.js +0 -18
- package/dist/sandbox-export-command-MO6DEZ7Z.js +0 -24
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.10
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -45,12 +45,12 @@ installed), so a plain file read resolves them at the version you have.
|
|
|
45
45
|
- [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.
|
|
46
46
|
- [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`).
|
|
47
47
|
- [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.
|
|
48
|
-
- [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
|
|
48
|
+
- [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
|
|
49
49
|
- [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.
|
|
50
50
|
- [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.
|
|
51
51
|
- [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
52
52
|
- [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, and each surface binds a different set of identifiers.
|
|
53
|
-
- [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/
|
|
53
|
+
- [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
|
|
54
54
|
- [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.
|
|
55
55
|
- [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.
|
|
56
56
|
|
|
@@ -115,7 +115,7 @@ which fails with a "must be ES modules" error until you switch it to module.
|
|
|
115
115
|
|
|
116
116
|
Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
|
|
117
117
|
without one a group's client paths are unresolvable until a lock exists: the bundle
|
|
118
|
-
exports fine and `xanots
|
|
118
|
+
exports fine and `xanots routes` / `getPath()` then fail on the very queries it just
|
|
119
119
|
built. An explicit `canonical` resolves them from the source alone.
|
|
120
120
|
|
|
121
121
|
Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
|
|
@@ -134,7 +134,7 @@ api-group/toolset canonical in a lock file written BESIDE THE ENTRY FILE —
|
|
|
134
134
|
To rename an object: rename in code, export (stderr prints the exact fix-up), run
|
|
135
135
|
`xanots lock rename <kind> <old> <new>`, export again — the original guid is emitted
|
|
136
136
|
under the new name, so the engine renames in place instead of delete+create. Taking
|
|
137
|
-
over an existing workspace: `xanots lock
|
|
137
|
+
over an existing workspace: `xanots lock import <its-packageExport.json>` first, then
|
|
138
138
|
export. Pruning, programmatic seeding, and which commands write the lock:
|
|
139
139
|
`llms/lock.md`.
|
|
140
140
|
|
|
@@ -147,19 +147,19 @@ environment and prints its URL.
|
|
|
147
147
|
AND records — before importing. The blast radius is a disposable environment, not a
|
|
148
148
|
production workspace, but confirm with the user before the first run.
|
|
149
149
|
|
|
150
|
-
**
|
|
150
|
+
**One destination, and no flag for it.**
|
|
151
151
|
|
|
152
|
-
-
|
|
153
|
-
(~1h; `--expires-hours` 1–72 at create time)
|
|
152
|
+
- `xanots deploy` writes to a NAMED, workspace-scoped, auto-expiring ephemeral tenant
|
|
153
|
+
(~1h; `--expires-hours` 1–72 at create time), and to nothing else — an `--env` here
|
|
154
|
+
is a usage error, not a choice. The active env is tracked in
|
|
154
155
|
`./.xano/ephemeral.json`, so deploying again REFRESHES it and the URL is unchanged;
|
|
155
156
|
if it expired or was swept, a fresh one is created and the new URL is called out.
|
|
156
157
|
`--static` puts the frontend ON THE EPHEMERAL, so backend and frontend share one
|
|
157
158
|
disposable environment.
|
|
158
159
|
⚠ Only the BACKEND URL survives a refresh: the replace clears static hosting too,
|
|
159
160
|
so `--static` publishes a NEW host every run and the previous URL stops serving.
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
not serve static hosting.
|
|
161
|
+
- `xanots status` names the env this project last deployed to, its URL and its expiry,
|
|
162
|
+
without your having to remember which one it was.
|
|
163
163
|
- `xanots release` promotes to your INSTANCE workspace and MERGES, not replaces:
|
|
164
164
|
adds/updates what you define, deletes nothing, writes no rows. Destruction is
|
|
165
165
|
opt-in per flag, previewed + confirmed, and can drop a table WITH its rows.
|
|
@@ -182,7 +182,7 @@ host falls back to '', and every call 404s off the dev server.
|
|
|
182
182
|
⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
|
|
183
183
|
by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
|
|
184
184
|
bare `XANO_HOST` token.
|
|
185
|
-
⚠ `xanots
|
|
185
|
+
⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
186
186
|
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
187
187
|
**Displaying a stored file.** A file column comes back as `{ path, name, type, size,
|
|
188
188
|
meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
|
|
@@ -332,7 +332,7 @@ Non-obvious authoring rules:
|
|
|
332
332
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
333
|
one, adds ~1 kB. So the cost is paid by importing ANY def at all, and reducing what a
|
|
334
334
|
def does will not reduce it.
|
|
335
|
-
Fix: `xanots
|
|
335
|
+
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
336
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
337
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
338
338
|
URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
|
|
@@ -358,7 +358,7 @@ Non-obvious authoring rules:
|
|
|
358
358
|
`s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
|
|
359
359
|
take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
|
|
360
360
|
`s.group(body)` and `s.util.post_process(body)` take it **positionally**.
|
|
361
|
-
`s.for` is **count-bounded** (`{ as, count
|
|
361
|
+
`s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
|
|
362
362
|
authored signatures in `llms/statements-data.md`.
|
|
363
363
|
- **MCP servers & agents are distinct root kinds** that both persist under the
|
|
364
364
|
`toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes
|
|
@@ -419,19 +419,10 @@ Non-obvious authoring rules:
|
|
|
419
419
|
credential the runner has. As a file that triple is `{ "type": "token",
|
|
420
420
|
"instance_base_url": …, "workspace_id": <n>, "meta_api_token": … }`. The older
|
|
421
421
|
`$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works but ROTATES: single-use.
|
|
422
|
-
- **Event-driven objects fire on an EPHEMERAL
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
run.
|
|
427
|
-
⚠ Under `--dest sandbox` they import cleanly but their stacks NEVER execute, and there
|
|
428
|
-
is no way to fire one manually — an insert on a bound table does not run its
|
|
429
|
-
`tableTrigger`, and the design silently does nothing. Only synchronously-invoked objects
|
|
430
|
-
(queries, functions, and the agents an endpoint calls with `s.ai.agent.run`) run there.
|
|
431
|
-
If you must stay on the sandbox, verify the logic out of band: factor the body into a
|
|
432
|
-
`defineFunction` (or a callable `query`) and invoke it directly — a `tableTrigger` that
|
|
433
|
-
screens a row on insert should delegate to a function a `query` can also call via
|
|
434
|
-
`s.function.run`, and you assert against that.
|
|
422
|
+
- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
|
|
423
|
+
and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
|
|
424
|
+
is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
|
|
425
|
+
cleanup, MCP tool call) by deploying it and letting it run.
|
|
435
426
|
- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
|
|
436
427
|
`["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
|
|
437
428
|
`0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
|
|
@@ -474,14 +465,14 @@ Control flow & blocks (each nests a sub-stack; block specials name it `body`):
|
|
|
474
465
|
- **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 })`).
|
|
475
466
|
- **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`.
|
|
476
467
|
- `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[]`.
|
|
477
|
-
- `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to.
|
|
468
|
+
- `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to; `count` is a `Value`, not a bare number.
|
|
478
469
|
- `s.foreach({ as, list, body })` — iterate `list`; `as` is the current item.
|
|
479
470
|
- `s.while({ when, body })` — `when` is a condition (`expr`/`cmp`/`and`/`or`).
|
|
480
471
|
- `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.
|
|
481
472
|
- `s.try_catch({ try, catch?, finally? })` — three `Statement[]` blocks.
|
|
482
473
|
- `s.group(body)` / `s.util.post_process(body)` — take a `Statement[]` **positionally**.
|
|
483
474
|
- `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` — nullary loop control.
|
|
484
|
-
- `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise.
|
|
475
|
+
- `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise; `exception` is a `Value`, not a bare string.
|
|
485
476
|
|
|
486
477
|
# Object kinds
|
|
487
478
|
|
|
@@ -539,7 +530,7 @@ to survive a rename).
|
|
|
539
530
|
- ⚠ 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.
|
|
540
531
|
- `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.
|
|
541
532
|
- `task({ name, guid?, description?, docs?, datasource?, active?, tags?, history?, schedule?, stack?, middleware? })` — a scheduled background job (function-like `stack`, no `input`/`response`).
|
|
542
|
-
- `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
|
|
533
|
+
- `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 (see Gotchas).
|
|
543
534
|
- `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> })`.
|
|
544
535
|
- `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.
|
|
545
536
|
- `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.
|
|
@@ -630,7 +621,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
630
621
|
|
|
631
622
|
- The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
|
|
632
623
|
- `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
|
|
633
|
-
- `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is text the raised message must CONTAIN; omit it to accept any error.
|
|
624
|
+
- `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
|
|
634
625
|
- `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` — which reads as a broken auth gate on a gate that works.
|
|
635
626
|
- An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.
|
|
636
627
|
- Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.
|
|
@@ -639,17 +630,18 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
639
630
|
|
|
640
631
|
`xanots test run-all` runs the unit tests AND the `workflowTest()` objects an environment carries. It takes no entry file and compiles nothing: it runs what is DEPLOYED, so deploy before testing.
|
|
641
632
|
|
|
642
|
-
- `--
|
|
633
|
+
- `--env ephemeral` (DEFAULT — the one this project last deployed to), `--env ephemeral:<name>`, or `--env workspace`. Same grammar as `init --from`. `deploy` takes no `--env` at all; `test` does, and `workspace` is allowed here because running a test only reads.
|
|
643
634
|
- `xanots test list` shows what is there without running it; `xanots test run "<name>"` runs one. When a name is ambiguous the error prints the qualified `function:math/happy path` form, which `run` also accepts.
|
|
644
635
|
- `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
|
|
645
636
|
- A failing suite exits 5, distinct from a crash. Tests that could not be REACHED exit 6 — retry that one, investigate the other. An environment with no tests is success, not failure.
|
|
637
|
+
- For CI, the exit code says THAT something failed and the JSON says WHICH. Every progress line goes to stderr and stdout carries one JSON document — emitted whenever stdout is not a terminal, or on demand with `--json`. `run-all` and `run`: `{ dest, env, total, passed, failed, tests: [{ kind, name, object?, status: "pass"|"fail", message?, timing? }] }`, with the same keys on an empty suite. `list` is `{ dest, env, total, tests: [...] }` and `deploy --test` nests the run under `testRun`. The per-test array is always `tests`.
|
|
646
638
|
- `xanots deploy ./index.ts --test` deploys and then runs the suite against what it just shipped. A failure exits 5 WITHOUT retracting the deploy — the environment is live either way.
|
|
647
639
|
|
|
648
640
|
# Agent and MCP def shapes
|
|
649
641
|
|
|
650
642
|
> Read when the workspace defines an `agent()` or an `mcpServer()`.
|
|
651
643
|
|
|
652
|
-
- `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`. 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 THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral
|
|
644
|
+
- `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`. 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 THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral.
|
|
653
645
|
- `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.
|
|
654
646
|
- `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 })`.
|
|
655
647
|
- `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.
|
|
@@ -727,7 +719,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
727
719
|
- **Tenant instances (isolated DB):** a tenant's realtime objects live in the TENANT's database, so BOTH halves of a client must name the tenant.
|
|
728
720
|
- Socket: `server.getUrl(base, { tenant })` → `/ws/<tenant>:<canonical>`. ⚠ A bare canonical on a tenant host resolves against the INSTANCE workspace instead.
|
|
729
721
|
- That colon form is PECULIAR TO THE SOCKET. Every other tenant URL gives the tenant its OWN segment — the HTTP half of the same client is `https://<host>/tenant/<tenant>/api:<canonical>/…`. NO request header is required for either.
|
|
730
|
-
- Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `
|
|
722
|
+
- Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `xanots status` prints (and that deploy injects as `window.XANO_HOST`) and the tenant is LIFTED into the socket form. So `getUrl(window.XANO_HOST)` needs no `{ tenant }`, and a CONFLICTING `{ tenant }` alongside it throws.
|
|
731
723
|
- ⚠ `getUrl`/`socketUrl` are NOT idempotent — a `baseUrl` that already carries a `/ws/<…>` path (an earlier result of either) THROWS. Resolve ONCE from the http(s) base; pass that result to `new WebSocket`, never back in as a base.
|
|
732
724
|
- Still pass `{ tenant }` explicitly for a tenant on its OWN DOMAIN — the hostname carries it for HTTP, but there is nothing in the URL for the socket to lift.
|
|
733
725
|
- ⚠ Tokens are tenant-scoped (audience `<tenant>:<license>`, not the bare license), so one minted through the instance workspace is REJECTED by a tenant's realtime server — authenticate and dial through the same tenant.
|
|
@@ -792,7 +784,8 @@ DB reads/writes (`table` is a def handle or name; `fieldName` defaults to the
|
|
|
792
784
|
primary key `id`):
|
|
793
785
|
|
|
794
786
|
- `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` — one row by field match; `output` restricts returned columns (and overrides column visibility — it can pull `internal` columns like a password hash).
|
|
795
|
-
- `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null`
|
|
787
|
+
- `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` for an id that names no row. Both spellings are live in pulled workspaces.
|
|
788
|
+
- ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`.
|
|
796
789
|
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
797
790
|
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
798
791
|
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
@@ -800,8 +793,9 @@ primary key `id`):
|
|
|
800
793
|
- `null` is accepted on EVERY column, including ones that refuse every other literal, and encodes `const:null` — a write OF null, not the same as omitting the key. A column's `nullable` is not consulted at encode; the engine refuses a null it forbids.
|
|
801
794
|
- Omitting a key on `add` writes the column's type default — declared `default` if set, `[]` for a list, `{}` for obj/json, else `null`. That `null` is EMITTED, not what the row holds: the engine applies the column's nullability, so a `nullable` column keeps `null` and a non-nullable one lands on its type's zero value (`""`, `0`, `false`). Set the cell when the stored value matters. On `edit` an omitted key keeps its stored value.
|
|
802
795
|
- An `f.password()` cell takes the PLAINTEXT — the column hashes on write, so a pre-hashed value, or a hashing filter on the cell, stores a hash of a hash that `security.check_password` can never match.
|
|
796
|
+
- A `table({ seed })` cell hashes the same way: the import writes the plaintext through the column's own rules, so a seeded credential matches under `security.check_password` exactly as an added one does. Demo accounts work as fixtures — the usual caution about seed data applies, since the plaintext sits in the repo.
|
|
803
797
|
- `s.db.edit({ table, fieldName?, fieldValue, row?, data?, output?, as? })` — update by field match.
|
|
804
|
-
- `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial
|
|
798
|
+
- `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial. ⚠ Unlike `db.edit`'s `row`, `data` is a single object `Value` — write `obj({ unread: c.int(0) })`, not the column-keyed record `row` takes.
|
|
805
799
|
On these three, `output` restricts the columns of the RETURNED row only — it does not change
|
|
806
800
|
what is written. Not offered on `db.del`/`db.has` (their result is a scalar) or on
|
|
807
801
|
`db.add_or_edit` (no output envelope).
|
|
@@ -810,6 +804,7 @@ primary key `id`):
|
|
|
810
804
|
- `where` / `additionalWhere` — `expr(...)`, an `expr[]` (ANDed), or a raw `Value`. Rides `context.search`.
|
|
811
805
|
- ⚠ `ignoreEmpty` DROPS the predicate when the operand is empty — it does not match zero rows. On an `in` comparison an empty list therefore returns the UNFILTERED set, so never use it to scope rows to a permitted-id list: an empty list of permissions returns everything.
|
|
812
806
|
- For the full operator set use `cmp(left, op, right, { ignoreEmpty? })` — `op`: `in`/`not in`/`like`/`ilike`/`between`/`contains`/`includes`/`overlaps`/`@>`/`~`/`search`/… plus the `expr` comparisons. Database-only — a runtime condition takes the `expr` set only.
|
|
807
|
+
- ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
|
|
813
808
|
- Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
|
|
814
809
|
- An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
|
|
815
810
|
- ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
|
|
@@ -890,7 +885,7 @@ Runtime behavior (what the `as:` output holds, and misses):
|
|
|
890
885
|
|
|
891
886
|
# Auth, cross-object calls, and microservices
|
|
892
887
|
|
|
893
|
-
> Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
|
|
888
|
+
> Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
|
|
894
889
|
|
|
895
890
|
Auth & calls:
|
|
896
891
|
|
|
@@ -906,6 +901,7 @@ Auth & calls:
|
|
|
906
901
|
- `s.api.call({ api, input?, headers?, auth?, as? })` — invoke an endpoint. `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) — a bare name is refused, because a query's identity is composed from its api group, verb, and name. `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`'s — a `{ "Name": value }` record (values may be tagged), a `string[]` of `"Name: value"` lines, or one `Value`. `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING — a tagged `Value` deploys clean and then fails the run with `Param: token - Text filter requires an integer, float, string or boolean value`, since the engine stores that slot as plain text and never evaluates it. Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`) — elsewhere it deploys clean and 500s the first real request.
|
|
907
902
|
- `s.api.request({ url, method?, params?, headers?, timeout?, follow_location?, verify_host?, verify_peer?, ca_certificate?, certificate?, certificate_pass?, private_key?, private_key_pass?, description?, output?, as? })` — external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`: `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH), `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter — the same record-of-values shape `response: { key: value }` takes); a tagged value NESTED inside an object or array THROWS at encode, so wrap a structured body in `obj({...})`, which encodes any depth as one `const:expr2` (→ query string for GET/HEAD/OPTIONS, body otherwise), `headers` a `{ "Name": value }` record whose values may be tagged (`{ "x-api-key": env("KEY") }`, each pair joined to a `"Name: value"` line) **or** a `string[]` of full header lines — prefer a header over a `?key=` query param for a credential — a URL travels into access logs, proxies and `Referer`. ⚠ Neither spelling is envelope-safe: the `as` envelope's `request` half mirrors `url`, `params` AND `headers`, so never return it raw from a credentialed request — read `response.result`. A NAME outside the header-token charset is refused, and a LITERAL value carrying a newline is refused; a value may hold `:` and spaces (`Bearer a: b` is a valid value). A TAGGED value cannot be checked at build time, so strip CRLF from caller-controlled input before this slot. Literal pairs lead the emitted array and computed ones follow, so the wire order is not the record's key order, `timeout` a `number` in seconds (1–86400), and `follow_location`/`verify_host`/`verify_peer` booleans. `description` (Settings tab) and `output` filters (Output tab) ride the envelope. SSL cert interdependencies (certificate↔private_key, ca_certificate→verify_peer) are checked at build time when statically provable, else by the engine at runtime. The `as` result is typed as the `{request, response}` envelope (`response.status: number`, `response.result: unknown`), so `InferResponse` resolves a `ref` to it. Same typed result on `webflow.request` and `microservice.request`.
|
|
908
903
|
- `s.stream.from_request({ url, method?, …tls, as? })` — streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope). `url` is REQUIRED — it shares `api.request`'s engine declaration, which has no default.
|
|
904
|
+
- `s.util.send_email({ to, subject, message, from?, cc?, bcc?, reply_to?, service_provider?, api_key?, scheduled_at?, as? })` — send email from the stack. `service_provider` is `"xano"` (the built-in mailer — needs NO `api_key` and no configuration, and does not require a verified sender) or `"resend"` (pass the key as `api_key: env("RESEND_API_KEY")`). Prefer this over hand-rolling `s.api.request` against a mail provider.
|
|
909
905
|
- `s.webflow.request({ path, method?, …tls, as? })` — Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied), and `path` is REQUIRED — the engine rejects an empty one. No `headers`: the engine builds its own from the workspace's Webflow connection and ignores an authored value.
|
|
910
906
|
- `s.task.call` / `s.tool.call` / `s.trigger.call` / `s.middleware.call` / `s.addon.call` — same `{ <target>, input?, as? }` shape against the named kind. `s.task.call` and `s.trigger.call` are WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`); `s.tool.call`, `s.middleware.call` and `s.addon.call` run from any stack.
|
|
911
907
|
- `s.action.call({ actionId, input?, registry?, as? })` / `s.action.package.call({ traceId, versionId, slug, input?, registry?, as? })` — invoke an installed action. Ids are SUPPLIED, never derived from a name: an action is installed onto the instance, so its identity is assigned at install and differs per instance — read them off a call in a pulled workspace. The package form needs all three parts; they are one composite and two of them address nothing. `registry` is the action's own settings, `input` the per-call arguments.
|
|
@@ -986,8 +982,9 @@ an `int`, and a null in it is unqueryable: `null` is never a legal `fieldValue`/
|
|
|
986
982
|
`s.db.get`/`edit`/`del` on that column answer HTTP 400 `Missing param: field_value` rather
|
|
987
983
|
than matching nothing. Declare `f.tableRef(users, { required: true, default: 0 })` for
|
|
988
984
|
"not set yet" — `s.db.get({ fieldName: "driver", fieldValue: c.int(0) })` matches no row and
|
|
989
|
-
binds `null`, which is the answer the null was reaching for
|
|
990
|
-
`
|
|
985
|
+
binds `null`, which is the answer the null was reaching for — and never `s.db.get_by_id`,
|
|
986
|
+
which validates `id >= 1` and fails the whole request on the sentinel. `export()` warns on a
|
|
987
|
+
literal `c.null()` in that slot.
|
|
991
988
|
An `f.vector(size)` column is SEARCHED through `s.db.query`'s `eval` pipeline, not through
|
|
992
989
|
any `SearchOp`: give the table `index: [{ type: "vector", fields: [{ name: "embedding", op:
|
|
993
990
|
"vector_cosine_ops" }] }]`, then rank with a distance filter + a sort on its alias (see
|
|
@@ -1361,7 +1358,7 @@ TypeScript annotations survive in the body, and top-level `await` works.
|
|
|
1361
1358
|
|
|
1362
1359
|
# Lock file
|
|
1363
1360
|
|
|
1364
|
-
> Read when a `xano.lock` exists or should — renaming/pruning/
|
|
1361
|
+
> Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
|
|
1365
1362
|
|
|
1366
1363
|
`xano.lock` pins each object's guid and each api-group/toolset canonical, so renames
|
|
1367
1364
|
stay renames (guids otherwise derive from `(type, name)`; a query's from `(api group,
|
|
@@ -1374,11 +1371,11 @@ old key drops automatically once its guid re-lands under the composed one).
|
|
|
1374
1371
|
- `xanots lock rename <kind> <old> <new>` — `kind` is the payload key (or `table`/`api_group`).
|
|
1375
1372
|
Run it after renaming in code; the next export emits the original guid under the new name.
|
|
1376
1373
|
- `xanots lock prune <entry-file> [keys…] --yes` — drops orphaned entries. Finding orphans
|
|
1377
|
-
RUNS the entry's module scope (env assertions included); `--
|
|
1374
|
+
RUNS the entry's module scope (env assertions included); `--identity-only --yes <kind:name>…`
|
|
1378
1375
|
prunes named keys with no evaluation and no orphan check.
|
|
1379
|
-
- `xanots lock
|
|
1376
|
+
- `xanots lock import <live-bundle.json> [--yes]` — seed the lock from an engine
|
|
1380
1377
|
packageExport when taking over an existing workspace.
|
|
1381
|
-
- Every lock subcommand accepts `--lock=<path>`. `rename`/`
|
|
1378
|
+
- Every lock subcommand accepts `--lock=<path>`. `rename`/`import` take no entry file, so
|
|
1382
1379
|
from outside the lock's directory pass `--lock` (or `--entry=<entry-file>` to derive it).
|
|
1383
1380
|
- Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing any def
|
|
1384
1381
|
module — references bake guids at import time, so late seeding is a silent no-op
|
|
@@ -1386,7 +1383,7 @@ old key drops automatically once its guid re-lands under the composed one).
|
|
|
1386
1383
|
|
|
1387
1384
|
What writes the lock: `export`/`deploy` of an ENTRY FILE update it via the shared compile
|
|
1388
1385
|
step — only when a lock exists or `--lock` is passed. Nothing from a DEPLOY is written
|
|
1389
|
-
back beyond that (an ephemeral
|
|
1386
|
+
back beyond that (an ephemeral is a separate workspace, so its identities must
|
|
1390
1387
|
not pollute yours). The one write-back is `release --replace`, which mints fresh
|
|
1391
1388
|
identities in the workspace the lock describes: it re-pins the lock from the rebuilt
|
|
1392
1389
|
workspace, because otherwise the next release matches nothing and duplicates every
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.10
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -45,12 +45,12 @@ installed), so a plain file read resolves them at the version you have.
|
|
|
45
45
|
- [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.
|
|
46
46
|
- [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`).
|
|
47
47
|
- [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.
|
|
48
|
-
- [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), or reaches a microservice.
|
|
48
|
+
- [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
|
|
49
49
|
- [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.
|
|
50
50
|
- [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.
|
|
51
51
|
- [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
52
52
|
- [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, and each surface binds a different set of identifiers.
|
|
53
|
-
- [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/
|
|
53
|
+
- [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
|
|
54
54
|
- [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.
|
|
55
55
|
- [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.
|
|
56
56
|
|
|
@@ -115,7 +115,7 @@ which fails with a "must be ES modules" error until you switch it to module.
|
|
|
115
115
|
|
|
116
116
|
Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
|
|
117
117
|
without one a group's client paths are unresolvable until a lock exists: the bundle
|
|
118
|
-
exports fine and `xanots
|
|
118
|
+
exports fine and `xanots routes` / `getPath()` then fail on the very queries it just
|
|
119
119
|
built. An explicit `canonical` resolves them from the source alone.
|
|
120
120
|
|
|
121
121
|
Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
|
|
@@ -134,7 +134,7 @@ api-group/toolset canonical in a lock file written BESIDE THE ENTRY FILE —
|
|
|
134
134
|
To rename an object: rename in code, export (stderr prints the exact fix-up), run
|
|
135
135
|
`xanots lock rename <kind> <old> <new>`, export again — the original guid is emitted
|
|
136
136
|
under the new name, so the engine renames in place instead of delete+create. Taking
|
|
137
|
-
over an existing workspace: `xanots lock
|
|
137
|
+
over an existing workspace: `xanots lock import <its-packageExport.json>` first, then
|
|
138
138
|
export. Pruning, programmatic seeding, and which commands write the lock:
|
|
139
139
|
`llms/lock.md`.
|
|
140
140
|
|
|
@@ -147,19 +147,19 @@ environment and prints its URL.
|
|
|
147
147
|
AND records — before importing. The blast radius is a disposable environment, not a
|
|
148
148
|
production workspace, but confirm with the user before the first run.
|
|
149
149
|
|
|
150
|
-
**
|
|
150
|
+
**One destination, and no flag for it.**
|
|
151
151
|
|
|
152
|
-
-
|
|
153
|
-
(~1h; `--expires-hours` 1–72 at create time)
|
|
152
|
+
- `xanots deploy` writes to a NAMED, workspace-scoped, auto-expiring ephemeral tenant
|
|
153
|
+
(~1h; `--expires-hours` 1–72 at create time), and to nothing else — an `--env` here
|
|
154
|
+
is a usage error, not a choice. The active env is tracked in
|
|
154
155
|
`./.xano/ephemeral.json`, so deploying again REFRESHES it and the URL is unchanged;
|
|
155
156
|
if it expired or was swept, a fresh one is created and the new URL is called out.
|
|
156
157
|
`--static` puts the frontend ON THE EPHEMERAL, so backend and frontend share one
|
|
157
158
|
disposable environment.
|
|
158
159
|
⚠ Only the BACKEND URL survives a refresh: the replace clears static hosting too,
|
|
159
160
|
so `--static` publishes a NEW host every run and the previous URL stops serving.
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
not serve static hosting.
|
|
161
|
+
- `xanots status` names the env this project last deployed to, its URL and its expiry,
|
|
162
|
+
without your having to remember which one it was.
|
|
163
163
|
- `xanots release` promotes to your INSTANCE workspace and MERGES, not replaces:
|
|
164
164
|
adds/updates what you define, deletes nothing, writes no rows. Destruction is
|
|
165
165
|
opt-in per flag, previewed + confirmed, and can drop a table WITH its rows.
|
|
@@ -182,7 +182,7 @@ host falls back to '', and every call 404s off the dev server.
|
|
|
182
182
|
⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
|
|
183
183
|
by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
|
|
184
184
|
bare `XANO_HOST` token.
|
|
185
|
-
⚠ `xanots
|
|
185
|
+
⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
186
186
|
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
187
187
|
**Displaying a stored file.** A file column comes back as `{ path, name, type, size,
|
|
188
188
|
meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
|
|
@@ -332,7 +332,7 @@ Non-obvious authoring rules:
|
|
|
332
332
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
333
|
one, adds ~1 kB. So the cost is paid by importing ANY def at all, and reducing what a
|
|
334
334
|
def does will not reduce it.
|
|
335
|
-
Fix: `xanots
|
|
335
|
+
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
336
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
337
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
338
338
|
URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
|
|
@@ -358,7 +358,7 @@ Non-obvious authoring rules:
|
|
|
358
358
|
`s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
|
|
359
359
|
take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
|
|
360
360
|
`s.group(body)` and `s.util.post_process(body)` take it **positionally**.
|
|
361
|
-
`s.for` is **count-bounded** (`{ as, count
|
|
361
|
+
`s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
|
|
362
362
|
authored signatures in `llms/statements-data.md`.
|
|
363
363
|
- **MCP servers & agents are distinct root kinds** that both persist under the
|
|
364
364
|
`toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes
|
|
@@ -419,19 +419,10 @@ Non-obvious authoring rules:
|
|
|
419
419
|
credential the runner has. As a file that triple is `{ "type": "token",
|
|
420
420
|
"instance_base_url": …, "workspace_id": <n>, "meta_api_token": … }`. The older
|
|
421
421
|
`$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still works but ROTATES: single-use.
|
|
422
|
-
- **Event-driven objects fire on an EPHEMERAL
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
run.
|
|
427
|
-
⚠ Under `--dest sandbox` they import cleanly but their stacks NEVER execute, and there
|
|
428
|
-
is no way to fire one manually — an insert on a bound table does not run its
|
|
429
|
-
`tableTrigger`, and the design silently does nothing. Only synchronously-invoked objects
|
|
430
|
-
(queries, functions, and the agents an endpoint calls with `s.ai.agent.run`) run there.
|
|
431
|
-
If you must stay on the sandbox, verify the logic out of band: factor the body into a
|
|
432
|
-
`defineFunction` (or a callable `query`) and invoke it directly — a `tableTrigger` that
|
|
433
|
-
screens a row on insert should delegate to a function a `query` can also call via
|
|
434
|
-
`s.function.run`, and you assert against that.
|
|
422
|
+
- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
|
|
423
|
+
and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
|
|
424
|
+
is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
|
|
425
|
+
cleanup, MCP tool call) by deploying it and letting it run.
|
|
435
426
|
- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
|
|
436
427
|
`["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
|
|
437
428
|
`0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
|
|
@@ -474,11 +465,11 @@ Control flow & blocks (each nests a sub-stack; block specials name it `body`):
|
|
|
474
465
|
- **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 })`).
|
|
475
466
|
- **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`.
|
|
476
467
|
- `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[]`.
|
|
477
|
-
- `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to.
|
|
468
|
+
- `s.for({ as, count, body })` — **count-bounded** loop (`as` is the index), NOT from/to; `count` is a `Value`, not a bare number.
|
|
478
469
|
- `s.foreach({ as, list, body })` — iterate `list`; `as` is the current item.
|
|
479
470
|
- `s.while({ when, body })` — `when` is a condition (`expr`/`cmp`/`and`/`or`).
|
|
480
471
|
- `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.
|
|
481
472
|
- `s.try_catch({ try, catch?, finally? })` — three `Statement[]` blocks.
|
|
482
473
|
- `s.group(body)` / `s.util.post_process(body)` — take a `Statement[]` **positionally**.
|
|
483
474
|
- `s.foreach_break()` / `s.foreach_continue()` / `s.foreach_remove()` — nullary loop control.
|
|
484
|
-
- `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise.
|
|
475
|
+
- `s.expect.to_throw({ body, exception? })` — `body` is the statements expected to raise; `exception` is a `Value`, not a bare string.
|