@xanots/sdk 0.0.9 → 0.0.11
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 +34 -0
- package/README.md +66 -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 +10 -8
- package/dist/{branch-commands-PBQ36L4A.js → branch-commands-2BLOC2GR.js} +13 -11
- package/dist/bundle.d.ts +218 -0
- package/dist/bundle.js +143 -0
- package/dist/{capture-7PO6SGB4.js → capture-4WVJY4DQ.js} +2 -2
- package/dist/{chunk-OW2QZEWL.js → chunk-22TKBSDV.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-6Z5CNWFC.js → chunk-4IF54NU5.js} +37 -6
- package/dist/{chunk-KSV7VOEX.js → chunk-4Q7ZOHH7.js} +3 -3
- package/dist/{chunk-QTNO2WD6.js → chunk-7ZYW652H.js} +1 -1
- package/dist/{chunk-4PTSTDIG.js → chunk-AIZKXUNP.js} +2 -2
- package/dist/{chunk-P5C2YKAM.js → chunk-ANUDXFEX.js} +18 -27
- package/dist/{chunk-4BXJGVZ3.js → chunk-BC2C5GVI.js} +2 -69
- package/dist/chunk-BSK7ELHU.js +70 -0
- 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-EHP3WPEG.js +21 -0
- package/dist/{chunk-JQLR64UC.js → chunk-F6CYJ7TN.js} +21 -6
- package/dist/{chunk-BKDJN76G.js → chunk-FE5I6S6N.js} +1 -1
- package/dist/{chunk-5YQOIFT4.js → chunk-G4EJMQLD.js} +2 -2
- package/dist/{chunk-L6TPHWAU.js → chunk-HYBN4H3F.js} +60 -86
- 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-OHX6MIUZ.js +184 -0
- package/dist/{chunk-UDL46O35.js → chunk-OWGCOGKK.js} +155 -3
- package/dist/{chunk-QONOJO45.js → chunk-QK7ZQJLP.js} +140 -20
- package/dist/{chunk-3QW5NFZJ.js → chunk-QKM4U5UK.js} +3 -3
- package/dist/{chunk-7OXFMCTX.js → chunk-QYMAZRAU.js} +7 -7
- package/dist/{chunk-K3YSQDXJ.js → chunk-RCT7UX7B.js} +92 -75
- package/dist/{chunk-T4XPCJRF.js → chunk-TCFIPDB3.js} +1 -1
- package/dist/{chunk-ZZLPTHT5.js → chunk-UOZMSF4C.js} +15 -8
- package/dist/{chunk-7JDT4PBU.js → chunk-VAF6A3YD.js} +5 -179
- package/dist/{chunk-6DHBYBTO.js → chunk-VNQM3V2C.js} +1 -2
- package/dist/chunk-WGPXT2G2.js +22 -0
- package/dist/{chunk-6AAT2AYQ.js → chunk-WP4OZZV4.js} +2 -2
- package/dist/{chunk-OYMR5AMJ.js → chunk-XEOX6AM7.js} +2 -2
- package/dist/{chunk-MU3O43L2.js → chunk-YBC3IKMF.js} +2 -2
- package/dist/chunk-ZQ2PKR6R.js +40 -0
- package/dist/cli.d.ts +57 -31
- package/dist/cli.js +9 -7
- package/dist/codegen-command-FUT2KJB6.js +49 -0
- package/dist/codegen.d.ts +2 -1
- package/dist/{completion-A6BZ3XGU.js → completion-WF46272M.js} +2 -2
- package/dist/{config-NL33PN4D.js → config-476F3PT5.js} +1 -1
- package/dist/{deploy-command-5TTI4TUP.js → deploy-command-IP7V7GT4.js} +69 -100
- package/dist/{ephemeral-command-7TK52YQM.js → ephemeral-command-U4AQ3TXX.js} +25 -28
- package/dist/index.d.ts +6 -4
- package/dist/index.js +10 -9
- package/dist/init-command-NPVL32L6.js +34 -0
- package/dist/{onboard-command-EHOHKQQU.js → init-web-3JNFG6GI.js} +10 -10
- package/dist/internal.d.ts +4 -4
- package/dist/internal.js +86 -73
- package/dist/io-P2H75UV2.js +12 -0
- package/dist/{live-diff-QK4KC2FJ.js → live-diff-IXKBVG4K.js} +3 -3
- package/dist/{lock-GFXD6G2E.js → lock-46FWYE4D.js} +3 -2
- package/dist/{lock-commands-ET7NIOLH.js → lock-commands-ZZKZ4LZJ.js} +23 -21
- 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 +5 -4
- package/dist/node.js +15 -13
- package/dist/{validate-command-SK5FPCV7.js → preflight-command-K346GPTY.js} +20 -18
- package/dist/{profile-command-QJAAUZIV.js → profile-command-LJSBDV2L.js} +5 -5
- package/dist/{release-command-4EXVLYV2.js → release-command-IMNTIVWK.js} +42 -38
- package/dist/response-BQVQ24l1.d.ts +844 -0
- 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-BLyNeQ8S.d.ts} +21 -8
- package/dist/{test-command-2F5LKMI6.js → test-command-YAZLKLGQ.js} +45 -28
- package/dist/{upgrade-command-QL523I62.js → upgrade-command-BN3EHAOI.js} +16 -14
- package/dist/{workspace-K72NP7SX.js → workspace-2COHDBM3.js} +1 -1
- package/dist/{workspace-command-HYA6JEKK.js → workspace-command-MNK7Y7MQ.js} +36 -34
- package/dist/{workspace-export-AJMGN3CQ.js → workspace-export-DURY5WYL.js} +2 -2
- package/dist/{response-CVAE2kMj.d.ts → xdo-BjJj5W_E.d.ts} +1 -837
- package/guides/README.md +2 -2
- 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 +2 -2
- package/llms/filters.md +11 -10
- package/llms/kinds-agent-mcp.md +1 -1
- package/llms/kinds-core.md +2 -2
- package/llms/kinds-realtime.md +1 -1
- package/llms/lock.md +5 -5
- package/llms/statements-data.md +3 -1
- package/llms/tests.md +1 -1
- package/llms/values.md +1 -1
- package/llms-full.txt +65 -63
- package/llms.txt +40 -41
- package/manifest.json +96 -427
- package/package.json +7 -2
- package/dist/chunk-3EYUR3TX.js +0 -100
- package/dist/chunk-JQJPFUZI.js +0 -118
- package/dist/chunk-YNZFIY4J.js +0 -78
- package/dist/codegen-command-YSCSCVT5.js +0 -43
- package/dist/env-target-4T7AT347.js +0 -16
- package/dist/init-command-OVFFW4QS.js +0 -30
- package/dist/io-M7XZEMK7.js +0 -11
- package/dist/sandbox-details-command-EDO56IY3.js +0 -18
- package/dist/sandbox-export-command-MO6DEZ7Z.js +0 -24
package/llms/tests.md
CHANGED
|
@@ -51,7 +51,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
51
51
|
|
|
52
52
|
`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.
|
|
53
53
|
|
|
54
|
-
- `--
|
|
54
|
+
- `--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.
|
|
55
55
|
- `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.
|
|
56
56
|
- `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
|
|
57
57
|
- 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.
|
package/llms/values.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- `c.array(a: Json[]) => Value` — Array constant (JSON string) → tag "const:array". Plain JSON literals only — a nested tagged value is rejected, same as c.obj.
|
|
13
13
|
- `c.expression(source: string) => Value` — Xano Expression Engine source, passed through VERBATIM → tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). ⚠️ NOT VALIDATED — never parsed or type-checked, invisible to InferResponse, and untouched by a rename that updates every typed ref(); a typo surfaces at runtime or as a wrong answer. Use it ONLY for syntax the typed surfaces cannot express (~ concatenation, inline arithmetic, conditionals) — prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.
|
|
14
14
|
- `c.now() => Value` — Current time as epoch-ms — the engine-native const:epochms constant (no filter). Valid inline as a where/cmp operand. For cutoff math (cutoff = now - max_age) either compare inline or, for reuse/readability, hoist it into an s.set_var and compare against the var.
|
|
15
|
-
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
15
|
+
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
16
16
|
- `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.
|
|
17
17
|
- `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it — an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).
|
|
18
18
|
- `col(name: string) => Value` — Reference a table column → tag "col".
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.11
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -23,7 +23,9 @@ every gotcha, and control flow. Per-surface detail lives in the topic files list
|
|
|
23
23
|
below — open the one whose condition matches the task, and skip the rest. For
|
|
24
24
|
exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
|
|
25
25
|
defaults, a filter's complete argument list, the engine `storedName` mapping — do a
|
|
26
|
-
TARGETED lookup in the shipped `manifest.json` (
|
|
26
|
+
TARGETED lookup in the shipped `manifest.json` (a program imports it as
|
|
27
|
+
`@xanots/sdk/manifest.json`, which Node ESM needs `with { type: "json" }` on;
|
|
28
|
+
grep or `jq` the one entry you need;
|
|
27
29
|
it is ~65k 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:
|
|
28
30
|
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
29
31
|
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
@@ -50,7 +52,7 @@ installed), so a plain file read resolves them at the version you have.
|
|
|
50
52
|
- [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
53
|
- [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
52
54
|
- [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/
|
|
55
|
+
- [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
56
|
- [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
57
|
- [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
58
|
|
|
@@ -115,7 +117,7 @@ which fails with a "must be ES modules" error until you switch it to module.
|
|
|
115
117
|
|
|
116
118
|
Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
|
|
117
119
|
without one a group's client paths are unresolvable until a lock exists: the bundle
|
|
118
|
-
exports fine and `xanots
|
|
120
|
+
exports fine and `xanots routes` / `getPath()` then fail on the very queries it just
|
|
119
121
|
built. An explicit `canonical` resolves them from the source alone.
|
|
120
122
|
|
|
121
123
|
Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
|
|
@@ -134,7 +136,7 @@ api-group/toolset canonical in a lock file written BESIDE THE ENTRY FILE —
|
|
|
134
136
|
To rename an object: rename in code, export (stderr prints the exact fix-up), run
|
|
135
137
|
`xanots lock rename <kind> <old> <new>`, export again — the original guid is emitted
|
|
136
138
|
under the new name, so the engine renames in place instead of delete+create. Taking
|
|
137
|
-
over an existing workspace: `xanots lock
|
|
139
|
+
over an existing workspace: `xanots lock import <its-packageExport.json>` first, then
|
|
138
140
|
export. Pruning, programmatic seeding, and which commands write the lock:
|
|
139
141
|
`llms/lock.md`.
|
|
140
142
|
|
|
@@ -147,19 +149,19 @@ environment and prints its URL.
|
|
|
147
149
|
AND records — before importing. The blast radius is a disposable environment, not a
|
|
148
150
|
production workspace, but confirm with the user before the first run.
|
|
149
151
|
|
|
150
|
-
**
|
|
152
|
+
**One destination, and no flag for it.**
|
|
151
153
|
|
|
152
|
-
-
|
|
153
|
-
(~1h; `--expires-hours` 1–72 at create time)
|
|
154
|
+
- `xanots deploy` writes to a NAMED, workspace-scoped, auto-expiring ephemeral tenant
|
|
155
|
+
(~1h; `--expires-hours` 1–72 at create time), and to nothing else — an `--env` here
|
|
156
|
+
is a usage error, not a choice. The active env is tracked in
|
|
154
157
|
`./.xano/ephemeral.json`, so deploying again REFRESHES it and the URL is unchanged;
|
|
155
158
|
if it expired or was swept, a fresh one is created and the new URL is called out.
|
|
156
159
|
`--static` puts the frontend ON THE EPHEMERAL, so backend and frontend share one
|
|
157
160
|
disposable environment.
|
|
158
161
|
⚠ Only the BACKEND URL survives a refresh: the replace clears static hosting too,
|
|
159
162
|
so `--static` publishes a NEW host every run and the previous URL stops serving.
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
not serve static hosting.
|
|
163
|
+
- `xanots status` names the env this project last deployed to, its URL and its expiry,
|
|
164
|
+
without your having to remember which one it was.
|
|
163
165
|
- `xanots release` promotes to your INSTANCE workspace and MERGES, not replaces:
|
|
164
166
|
adds/updates what you define, deletes nothing, writes no rows. Destruction is
|
|
165
167
|
opt-in per flag, previewed + confirmed, and can drop a table WITH its rows.
|
|
@@ -182,7 +184,7 @@ host falls back to '', and every call 404s off the dev server.
|
|
|
182
184
|
⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
|
|
183
185
|
by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
|
|
184
186
|
bare `XANO_HOST` token.
|
|
185
|
-
⚠ `xanots
|
|
187
|
+
⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
186
188
|
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
187
189
|
**Displaying a stored file.** A file column comes back as `{ path, name, type, size,
|
|
188
190
|
meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
|
|
@@ -266,6 +268,10 @@ Non-obvious authoring rules:
|
|
|
266
268
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
269
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
270
|
check to the client.
|
|
271
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
272
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
273
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
274
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
269
275
|
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
276
|
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
277
|
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
@@ -320,19 +326,20 @@ Non-obvious authoring rules:
|
|
|
320
326
|
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
321
327
|
either way; only `deploy` ships them. The `node:fs` writers
|
|
322
328
|
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
323
|
-
NOT the browser-safe `@xanots/sdk` entry (
|
|
324
|
-
|
|
329
|
+
NOT the browser-safe `@xanots/sdk` entry (a frontend imports defs from it for
|
|
330
|
+
`getPath()`/`InferInput`).
|
|
325
331
|
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
326
332
|
the lock model) is on `@xanots/sdk/internal` and is never needed to author.
|
|
333
|
+
READING a bundle back is `@xanots/sdk/bundle` — a statement walker (`2.if.0` paths),
|
|
334
|
+
a structural hash, `mvp:*` catalog, `tableRefOf`.
|
|
327
335
|
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
328
336
|
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
329
337
|
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
330
338
|
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
331
339
|
⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
|
|
332
340
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
|
-
one, adds ~1 kB
|
|
334
|
-
|
|
335
|
-
Fix: `xanots paths <entry> --emit xano/routes.gen.ts` (`routes` is an accepted alias) — verbs, paths, and sockets as
|
|
341
|
+
one, adds ~1 kB — so reducing what a def does will not reduce it.
|
|
342
|
+
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
343
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
344
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
338
345
|
URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
|
|
@@ -385,17 +392,18 @@ Non-obvious authoring rules:
|
|
|
385
392
|
return.
|
|
386
393
|
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
387
394
|
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
388
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped
|
|
389
|
-
`c.text("
|
|
390
|
-
input, so a precondition on it silently rejects all values
|
|
391
|
-
`c.regex(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped, and the
|
|
396
|
+
ARGUMENT is the subject. A bare `c.text("^…$")` is an invalid pattern that matches
|
|
397
|
+
*nothing* for every input, so a precondition on it silently rejects all values.
|
|
398
|
+
`c.regex(body, "i")` wraps + escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
|
|
399
|
+
Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
|
|
400
|
+
answers false for EVERY input, so an `if (matches) reject` guard admits what it
|
|
401
|
+
refuses. `withFilters` throws on a bare `c.text` pattern from ANY position in
|
|
402
|
+
the chain (a normalizer in front of the regex filter is refused too; nothing upstream
|
|
403
|
+
adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match`
|
|
404
|
+
is the same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
|
|
405
|
+
`export()` warns on a reversed pair in stored bytes. Better still:
|
|
406
|
+
a native typed input (`input.email`) over hand-rolled validation.
|
|
399
407
|
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
400
408
|
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
401
409
|
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
|
@@ -419,19 +427,10 @@ Non-obvious authoring rules:
|
|
|
419
427
|
credential the runner has. As a file that triple is `{ "type": "token",
|
|
420
428
|
"instance_base_url": …, "workspace_id": <n>, "meta_api_token": … }`. The older
|
|
421
429
|
`$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.
|
|
430
|
+
- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
|
|
431
|
+
and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
|
|
432
|
+
is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
|
|
433
|
+
cleanup, MCP tool call) by deploying it and letting it run.
|
|
435
434
|
- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
|
|
436
435
|
`["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
|
|
437
436
|
`0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
|
|
@@ -531,7 +530,7 @@ to survive a rename).
|
|
|
531
530
|
- `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
|
|
532
531
|
- `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
|
|
533
532
|
- `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
|
|
534
|
-
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
533
|
+
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param. Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
535
534
|
- **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
|
|
536
535
|
- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
|
|
537
536
|
- `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
|
|
@@ -539,7 +538,7 @@ to survive a rename).
|
|
|
539
538
|
- ⚠ 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
539
|
- `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
540
|
- `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
|
|
541
|
+
- `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
542
|
- `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
543
|
- `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
544
|
- `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.
|
|
@@ -639,7 +638,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
639
638
|
|
|
640
639
|
`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
640
|
|
|
642
|
-
- `--
|
|
641
|
+
- `--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
642
|
- `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
643
|
- `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
|
|
645
644
|
- 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.
|
|
@@ -650,7 +649,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
650
649
|
|
|
651
650
|
> Read when the workspace defines an `agent()` or an `mcpServer()`.
|
|
652
651
|
|
|
653
|
-
- `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
|
|
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.
|
|
654
653
|
- `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.
|
|
655
654
|
- `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 })`.
|
|
656
655
|
- `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.
|
|
@@ -728,7 +727,7 @@ The run is isolated in ways that make a correct test fail for reasons the failur
|
|
|
728
727
|
- **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.
|
|
729
728
|
- Socket: `server.getUrl(base, { tenant })` → `/ws/<tenant>:<canonical>`. ⚠ A bare canonical on a tenant host resolves against the INSTANCE workspace instead.
|
|
730
729
|
- 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.
|
|
731
|
-
- Because the shapes differ, `getUrl` TRANSLATES a tenant base URL instead of concatenating: pass the `https://<host>/tenant/<name>` that `
|
|
730
|
+
- 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.
|
|
732
731
|
- ⚠ `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.
|
|
733
732
|
- 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.
|
|
734
733
|
- ⚠ 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.
|
|
@@ -794,7 +793,7 @@ primary key `id`):
|
|
|
794
793
|
|
|
795
794
|
- `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).
|
|
796
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` for an id that names no row. Both spellings are live in pulled workspaces.
|
|
797
|
-
- ⚠ `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
|
+
- ⚠ `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 })`. `export()` warns when the `id` is statically a `0` — the literal `c.int(0)`, an `inp()` whose declared input default is `0`, or a `ref()` to a column declared `default: 0` — and `--strict` fails the build.
|
|
798
797
|
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
799
798
|
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
800
799
|
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
@@ -836,6 +835,8 @@ primary key `id`):
|
|
|
836
835
|
- `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
|
|
837
836
|
- `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
|
|
838
837
|
- `s.db.direct_query({ sql, responseType?, args?, parser?, as? })` — `sql` is a **raw string** (not a `Value`); binds go in `args: Value[]`. `parser: "template_engine"` renders the body as a template first — how a query interpolates a column or table name a bound arg cannot carry; omit it for the default.
|
|
838
|
+
- Template placeholders are Twig over the request scope: `{{ $input.name }}` for an input, `{{ $var.name }}` for a stack variable. ⚠ A BARE `{{ name }}` renders as the empty string — HTTP 200, no error, a query that silently ran with a blank where the value belonged. A bound `?` arg carries a VALUE without the template at all.
|
|
839
|
+
- ⚠ A table's PHYSICAL name is **not stable across deploys**. A deploy is a full replace, so every table is created afresh and the id in its name moves every time — the same unchanged project redeployed three times gave one table three different names. Never store, cache, hardcode or fixture one: resolve it from `information_schema` inside the same request that uses it.
|
|
839
840
|
- `s.db.external.<engine>.direct_query({ sql, connectionString, responseType?, args?, parser?, as? })` — same shape against an EXTERNAL database; `<engine>` is `postgres`/`mysql`/`mssql`/`oracle`/`snowflake`. `connectionString` is a `Value` — reach for `env(...)`, not a literal — stored as `context.connection_string_flex`. A bare string stores the older `context.connection_string` instead (an env-var name unless it looks like a URL); each form round-trips as itself.
|
|
840
841
|
- `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
|
|
841
842
|
- `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` — `items` is an array `Value`.
|
|
@@ -938,7 +939,7 @@ Microservices (the `microservice()` def and the statement that calls it):
|
|
|
938
939
|
- `c.array(a: Json[]) => Value` — Array constant (JSON string) → tag "const:array". Plain JSON literals only — a nested tagged value is rejected, same as c.obj.
|
|
939
940
|
- `c.expression(source: string) => Value` — Xano Expression Engine source, passed through VERBATIM → tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). ⚠️ NOT VALIDATED — never parsed or type-checked, invisible to InferResponse, and untouched by a rename that updates every typed ref(); a typo surfaces at runtime or as a wrong answer. Use it ONLY for syntax the typed surfaces cannot express (~ concatenation, inline arithmetic, conditionals) — prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.
|
|
940
941
|
- `c.now() => Value` — Current time as epoch-ms — the engine-native const:epochms constant (no filter). Valid inline as a where/cmp operand. For cutoff math (cutoff = now - max_age) either compare inline or, for reuse/readability, hoist it into an s.set_var and compare against the var.
|
|
941
|
-
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
942
|
+
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
942
943
|
- `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.
|
|
943
944
|
- `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it — an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).
|
|
944
945
|
- `col(name: string) => Value` — Reference a table column → tag "col".
|
|
@@ -1110,12 +1111,13 @@ database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHER
|
|
|
1110
1111
|
which the DB applies atomically. Reserve the pipeline form for low-contention counters
|
|
1111
1112
|
where a rare lost update is acceptable.
|
|
1112
1113
|
⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
|
|
1113
|
-
does NOT expose: the engine derives
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
the
|
|
1118
|
-
|
|
1114
|
+
does NOT expose: the engine derives it from ids assigned at import — not knowable from a
|
|
1115
|
+
`table()` def (identity is a name + guid, not the numeric id) — and `sql_name` persists
|
|
1116
|
+
empty. The derived name is also NOT STABLE: a deploy is a full replace, so every table is
|
|
1117
|
+
created afresh and the id in its name moves each time, on the same unchanged project. So
|
|
1118
|
+
the safe counter drops out of the typed surface: resolve the physical name from
|
|
1119
|
+
`information_schema` inside the request that uses it, and never store, cache or hardcode
|
|
1120
|
+
one. A typed atomic path needs an engine change.
|
|
1119
1121
|
|
|
1120
1122
|
- `fl.add(value: decimal): decimal`
|
|
1121
1123
|
- `fl.append(value: <T>, path: text): <T>[]`
|
|
@@ -1207,11 +1209,11 @@ engine change.
|
|
|
1207
1209
|
- `fl.prepend(value: <T>, path: text): <T>[]`
|
|
1208
1210
|
- `fl.range(start: int, stop: int): int[]`
|
|
1209
1211
|
- `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
|
|
1210
|
-
- `fl.regex_match(subject: text): text[]`
|
|
1211
|
-
- `fl.regex_match_all(subject: text): text[]`
|
|
1212
|
+
- `fl.regex_match(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
|
|
1213
|
+
- `fl.regex_match_all(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
|
|
1212
1214
|
- `fl.regex_quote(delimiter?: text): text`
|
|
1213
|
-
- `fl.regex_replace(replacement: text, subject: text): text`
|
|
1214
|
-
- `fl.regex_test(subject: text): bool`
|
|
1215
|
+
- `fl.regex_replace(replacement: text, subject: text): text` — piped value is the PATTERN, `subject` is the text searched — see `regex_test`. The replacement comes FIRST
|
|
1216
|
+
- `fl.regex_test(subject: text): bool` — piped value is the PATTERN (build it with `c.regex(...)`); the arg is the subject — the REVERSE of `contains`/`starts_with`. Swapped, it answers false for every input with no error, so write `withFilters(c.regex("^a+$"), fl.regex_test(inp("s")))` (or name the arg: `fl.regex_test({ subject: inp("s") })`). A pattern found in the subject slot is refused at build time
|
|
1215
1217
|
- `fl.round(precision?: int): decimal`
|
|
1216
1218
|
- `fl.rtrim(mask?: text): text`
|
|
1217
1219
|
- `fl.secureid_decode(salt: text): int`
|
|
@@ -1367,7 +1369,7 @@ TypeScript annotations survive in the body, and top-level `await` works.
|
|
|
1367
1369
|
|
|
1368
1370
|
# Lock file
|
|
1369
1371
|
|
|
1370
|
-
> Read when a `xano.lock` exists or should — renaming/pruning/
|
|
1372
|
+
> Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
|
|
1371
1373
|
|
|
1372
1374
|
`xano.lock` pins each object's guid and each api-group/toolset canonical, so renames
|
|
1373
1375
|
stay renames (guids otherwise derive from `(type, name)`; a query's from `(api group,
|
|
@@ -1380,11 +1382,11 @@ old key drops automatically once its guid re-lands under the composed one).
|
|
|
1380
1382
|
- `xanots lock rename <kind> <old> <new>` — `kind` is the payload key (or `table`/`api_group`).
|
|
1381
1383
|
Run it after renaming in code; the next export emits the original guid under the new name.
|
|
1382
1384
|
- `xanots lock prune <entry-file> [keys…] --yes` — drops orphaned entries. Finding orphans
|
|
1383
|
-
RUNS the entry's module scope (env assertions included); `--
|
|
1385
|
+
RUNS the entry's module scope (env assertions included); `--identity-only --yes <kind:name>…`
|
|
1384
1386
|
prunes named keys with no evaluation and no orphan check.
|
|
1385
|
-
- `xanots lock
|
|
1387
|
+
- `xanots lock import <live-bundle.json> [--yes]` — seed the lock from an engine
|
|
1386
1388
|
packageExport when taking over an existing workspace.
|
|
1387
|
-
- Every lock subcommand accepts `--lock=<path>`. `rename`/`
|
|
1389
|
+
- Every lock subcommand accepts `--lock=<path>`. `rename`/`import` take no entry file, so
|
|
1388
1390
|
from outside the lock's directory pass `--lock` (or `--entry=<entry-file>` to derive it).
|
|
1389
1391
|
- Programmatic use: call `seedLockOverrides(readLockFile(path))` BEFORE importing any def
|
|
1390
1392
|
module — references bake guids at import time, so late seeding is a silent no-op
|
|
@@ -1392,7 +1394,7 @@ old key drops automatically once its guid re-lands under the composed one).
|
|
|
1392
1394
|
|
|
1393
1395
|
What writes the lock: `export`/`deploy` of an ENTRY FILE update it via the shared compile
|
|
1394
1396
|
step — only when a lock exists or `--lock` is passed. Nothing from a DEPLOY is written
|
|
1395
|
-
back beyond that (an ephemeral
|
|
1397
|
+
back beyond that (an ephemeral is a separate workspace, so its identities must
|
|
1396
1398
|
not pollute yours). The one write-back is `release --replace`, which mints fresh
|
|
1397
1399
|
identities in the workspace the lock describes: it re-pins the lock from the rebuilt
|
|
1398
1400
|
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.11
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -23,7 +23,9 @@ every gotcha, and control flow. Per-surface detail lives in the topic files list
|
|
|
23
23
|
below — open the one whose condition matches the task, and skip the rest. For
|
|
24
24
|
exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
|
|
25
25
|
defaults, a filter's complete argument list, the engine `storedName` mapping — do a
|
|
26
|
-
TARGETED lookup in the shipped `manifest.json` (
|
|
26
|
+
TARGETED lookup in the shipped `manifest.json` (a program imports it as
|
|
27
|
+
`@xanots/sdk/manifest.json`, which Node ESM needs `with { type: "json" }` on;
|
|
28
|
+
grep or `jq` the one entry you need;
|
|
27
29
|
it is ~65k 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:
|
|
28
30
|
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
29
31
|
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
@@ -50,7 +52,7 @@ installed), so a plain file read resolves them at the version you have.
|
|
|
50
52
|
- [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
53
|
- [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
52
54
|
- [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/
|
|
55
|
+
- [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
56
|
- [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
57
|
- [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
58
|
|
|
@@ -115,7 +117,7 @@ which fails with a "must be ES modules" error until you switch it to module.
|
|
|
115
117
|
|
|
116
118
|
Set `canonical` on every `apiGroup`. The engine mints the URL token server-side, so
|
|
117
119
|
without one a group's client paths are unresolvable until a lock exists: the bundle
|
|
118
|
-
exports fine and `xanots
|
|
120
|
+
exports fine and `xanots routes` / `getPath()` then fail on the very queries it just
|
|
119
121
|
built. An explicit `canonical` resolves them from the source alone.
|
|
120
122
|
|
|
121
123
|
Build warnings: `export()` prints the shapes that deploy clean and then do the wrong
|
|
@@ -134,7 +136,7 @@ api-group/toolset canonical in a lock file written BESIDE THE ENTRY FILE —
|
|
|
134
136
|
To rename an object: rename in code, export (stderr prints the exact fix-up), run
|
|
135
137
|
`xanots lock rename <kind> <old> <new>`, export again — the original guid is emitted
|
|
136
138
|
under the new name, so the engine renames in place instead of delete+create. Taking
|
|
137
|
-
over an existing workspace: `xanots lock
|
|
139
|
+
over an existing workspace: `xanots lock import <its-packageExport.json>` first, then
|
|
138
140
|
export. Pruning, programmatic seeding, and which commands write the lock:
|
|
139
141
|
`llms/lock.md`.
|
|
140
142
|
|
|
@@ -147,19 +149,19 @@ environment and prints its URL.
|
|
|
147
149
|
AND records — before importing. The blast radius is a disposable environment, not a
|
|
148
150
|
production workspace, but confirm with the user before the first run.
|
|
149
151
|
|
|
150
|
-
**
|
|
152
|
+
**One destination, and no flag for it.**
|
|
151
153
|
|
|
152
|
-
-
|
|
153
|
-
(~1h; `--expires-hours` 1–72 at create time)
|
|
154
|
+
- `xanots deploy` writes to a NAMED, workspace-scoped, auto-expiring ephemeral tenant
|
|
155
|
+
(~1h; `--expires-hours` 1–72 at create time), and to nothing else — an `--env` here
|
|
156
|
+
is a usage error, not a choice. The active env is tracked in
|
|
154
157
|
`./.xano/ephemeral.json`, so deploying again REFRESHES it and the URL is unchanged;
|
|
155
158
|
if it expired or was swept, a fresh one is created and the new URL is called out.
|
|
156
159
|
`--static` puts the frontend ON THE EPHEMERAL, so backend and frontend share one
|
|
157
160
|
disposable environment.
|
|
158
161
|
⚠ Only the BACKEND URL survives a refresh: the replace clears static hosting too,
|
|
159
162
|
so `--static` publishes a NEW host every run and the previous URL stops serving.
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
not serve static hosting.
|
|
163
|
+
- `xanots status` names the env this project last deployed to, its URL and its expiry,
|
|
164
|
+
without your having to remember which one it was.
|
|
163
165
|
- `xanots release` promotes to your INSTANCE workspace and MERGES, not replaces:
|
|
164
166
|
adds/updates what you define, deletes nothing, writes no rows. Destruction is
|
|
165
167
|
opt-in per flag, previewed + confirmed, and can drop a table WITH its rows.
|
|
@@ -182,7 +184,7 @@ host falls back to '', and every call 404s off the dev server.
|
|
|
182
184
|
⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
|
|
183
185
|
by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
|
|
184
186
|
bare `XANO_HOST` token.
|
|
185
|
-
⚠ `xanots
|
|
187
|
+
⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
186
188
|
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
187
189
|
**Displaying a stored file.** A file column comes back as `{ path, name, type, size,
|
|
188
190
|
meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
|
|
@@ -266,6 +268,10 @@ Non-obvious authoring rules:
|
|
|
266
268
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
269
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
270
|
check to the client.
|
|
271
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
272
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
273
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
274
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
269
275
|
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
276
|
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
277
|
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
@@ -320,19 +326,20 @@ Non-obvious authoring rules:
|
|
|
320
326
|
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
321
327
|
either way; only `deploy` ships them. The `node:fs` writers
|
|
322
328
|
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
323
|
-
NOT the browser-safe `@xanots/sdk` entry (
|
|
324
|
-
|
|
329
|
+
NOT the browser-safe `@xanots/sdk` entry (a frontend imports defs from it for
|
|
330
|
+
`getPath()`/`InferInput`).
|
|
325
331
|
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
326
332
|
the lock model) is on `@xanots/sdk/internal` and is never needed to author.
|
|
333
|
+
READING a bundle back is `@xanots/sdk/bundle` — a statement walker (`2.if.0` paths),
|
|
334
|
+
a structural hash, `mvp:*` catalog, `tableRefOf`.
|
|
327
335
|
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
328
336
|
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
329
337
|
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
330
338
|
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
331
339
|
⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
|
|
332
340
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
|
-
one, adds ~1 kB
|
|
334
|
-
|
|
335
|
-
Fix: `xanots paths <entry> --emit xano/routes.gen.ts` (`routes` is an accepted alias) — verbs, paths, and sockets as
|
|
341
|
+
one, adds ~1 kB — so reducing what a def does will not reduce it.
|
|
342
|
+
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
343
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
344
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
338
345
|
URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
|
|
@@ -385,17 +392,18 @@ Non-obvious authoring rules:
|
|
|
385
392
|
return.
|
|
386
393
|
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
387
394
|
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
388
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped
|
|
389
|
-
`c.text("
|
|
390
|
-
input, so a precondition on it silently rejects all values
|
|
391
|
-
`c.regex(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped, and the
|
|
396
|
+
ARGUMENT is the subject. A bare `c.text("^…$")` is an invalid pattern that matches
|
|
397
|
+
*nothing* for every input, so a precondition on it silently rejects all values.
|
|
398
|
+
`c.regex(body, "i")` wraps + escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
|
|
399
|
+
Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
|
|
400
|
+
answers false for EVERY input, so an `if (matches) reject` guard admits what it
|
|
401
|
+
refuses. `withFilters` throws on a bare `c.text` pattern from ANY position in
|
|
402
|
+
the chain (a normalizer in front of the regex filter is refused too; nothing upstream
|
|
403
|
+
adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match`
|
|
404
|
+
is the same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
|
|
405
|
+
`export()` warns on a reversed pair in stored bytes. Better still:
|
|
406
|
+
a native typed input (`input.email`) over hand-rolled validation.
|
|
399
407
|
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
400
408
|
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
401
409
|
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
|
@@ -419,19 +427,10 @@ Non-obvious authoring rules:
|
|
|
419
427
|
credential the runner has. As a file that triple is `{ "type": "token",
|
|
420
428
|
"instance_base_url": …, "workspace_id": <n>, "meta_api_token": … }`. The older
|
|
421
429
|
`$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.
|
|
430
|
+
- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
|
|
431
|
+
and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
|
|
432
|
+
is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
|
|
433
|
+
cleanup, MCP tool call) by deploying it and letting it run.
|
|
435
434
|
- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
|
|
436
435
|
`["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
|
|
437
436
|
`0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
|