@xanots/sdk 0.0.12 → 0.0.14
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 +39 -0
- package/README.md +5 -3
- package/dist/.build-fingerprint +1 -1
- package/dist/{agent-file-refresh-GQWAAOBV.js → agent-file-refresh-2LZ2YRFZ.js} +3 -3
- package/dist/bin.js +3 -3
- package/dist/{chunk-GSP2BY4F.js → chunk-BYQHCCYU.js} +1 -1
- package/dist/{chunk-AG5GCZDD.js → chunk-CII2TOE4.js} +2 -2
- package/dist/{chunk-2VTJSI6X.js → chunk-F3MRG7NI.js} +2 -2
- package/dist/{chunk-XT3XQ4PF.js → chunk-LISUHLWI.js} +11 -11
- package/dist/{chunk-QYSQ3UDO.js → chunk-MEFMTICH.js} +76 -1
- package/dist/{chunk-VPAWRBK5.js → chunk-ORZINVGQ.js} +33 -9
- package/dist/{chunk-VRNZ2NVV.js → chunk-PJNWOZMT.js} +2 -2
- package/dist/{chunk-PLE5QQOZ.js → chunk-UQMMFW32.js} +5 -5
- package/dist/{chunk-ELK7UALJ.js → chunk-UXPDQFG2.js} +2 -2
- package/dist/{chunk-VK26K7AY.js → chunk-XBIFQWZM.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/{codegen-command-Y2SPMAUW.js → codegen-command-5A6FVKUA.js} +7 -7
- package/dist/{deploy-command-XHS5PKPV.js → deploy-command-VBB7JYXH.js} +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -3
- package/dist/{init-command-23FFNUFT.js → init-command-SIJUH2DD.js} +5 -5
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +362 -264
- package/dist/{lock-commands-WCRC56ME.js → lock-commands-4TX3BONI.js} +3 -3
- package/dist/node.d.ts +2 -2
- package/dist/node.js +4 -4
- package/dist/{preflight-command-TZWPHBXY.js → preflight-command-MTYGPNVT.js} +4 -4
- package/dist/{release-command-WEFYRTCI.js → release-command-NXCPQDFF.js} +6 -6
- package/dist/{routes-manifest-5ZFKUQWA.js → routes-manifest-MN6XBYRE.js} +24 -13
- package/dist/{store-DAnUIi1T.d.ts → store-9Psd0jiF.d.ts} +34 -16
- package/dist/{upgrade-command-GYEIMJBG.js → upgrade-command-GSH3M3N6.js} +6 -6
- package/dist/{workspace-command-2ZTGT26W.js → workspace-command-XUOPLVQ4.js} +7 -7
- package/guides/README.md +1 -1
- package/guides/coverage.md +3 -2
- package/guides/typed-frontend.md +20 -3
- package/llms/client.md +44 -0
- package/llms/errors.md +33 -0
- package/llms/fields.md +26 -1
- package/llms/filters.md +4 -8
- package/llms/kinds-agent-mcp.md +1 -0
- package/llms/kinds-core.md +16 -2
- package/llms/kinds-knowledge.md +1 -1
- package/llms/kinds-realtime.md +3 -3
- package/llms/lambda.md +1 -1
- package/llms/legacy.md +1 -1
- package/llms/object-kinds.md +2 -2
- package/llms/statements-calls.md +25 -4
- package/llms/statements-catalog.md +1 -1
- package/llms/statements-runtime.md +20 -5
- package/llms/tests.md +3 -3
- package/llms/triggers.md +14 -2
- package/llms/values.md +2 -2
- package/llms-full.txt +362 -270
- package/llms.txt +163 -234
- package/manifest.json +4 -4
- package/package.json +1 -1
package/dist/internal.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
taskKind,
|
|
30
30
|
toolKind,
|
|
31
31
|
workflowTestKind
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-PJNWOZMT.js";
|
|
33
33
|
import {
|
|
34
34
|
GENERATED_SPECS,
|
|
35
35
|
GENERATED_STATEMENT_NAMES,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
registerSpec,
|
|
52
52
|
triggerKind,
|
|
53
53
|
workspaceKind
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-BYQHCCYU.js";
|
|
55
55
|
import {
|
|
56
56
|
IMPLEMENTED_STATEMENTS,
|
|
57
57
|
STATEMENT_SURFACES,
|
|
@@ -216,7 +216,7 @@ var KIND_DESCRIPTORS = [
|
|
|
216
216
|
{ kind: "mcp_server", payloadKey: "toolset", authorFactory: "mcpServer", description: "An MCP server exposing a set of tools to external MCP clients.", registerMethod: "registerMcpServers" },
|
|
217
217
|
{ kind: "agent", payloadKey: "toolset", authorFactory: "agent", description: "An AI agent: an LLM configuration plus the tools it can call. Invoke it from any stack (query/function/task/tool/trigger) with `s.ai.agent.run` \u2014 no public endpoint; the result is a rich envelope whose completion text is at `.result`.", registerMethod: "registerAgents" },
|
|
218
218
|
{ kind: "task", payloadKey: "task", authorFactory: "task", description: "A scheduled background job (cron/interval) that runs a stack on a timer.", registerMethod: "registerTasks" },
|
|
219
|
-
{ kind: "workflow_test", payloadKey: "workflow_test", authorFactory: "workflowTest", description: 'An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one
|
|
219
|
+
{ kind: "workflow_test", payloadKey: "workflow_test", authorFactory: "workflowTest", description: 'An end-to-end test: a named stack with NO input and NO response that invokes other objects (`s.function.call`, `s.task.call`, `s.api.call`) and asserts on what they bind with `s.expect.*`. `datasource` defaults to `""` (an EMPTY datasource, recommended); naming one is the clone trap described in `llms/kinds-core.md`.', registerMethod: "registerWorkflowTests" },
|
|
220
220
|
{ kind: "middleware", payloadKey: "middleware", authorFactory: "middleware", description: "A reusable pre/post stack attached to a query/function/task/tool/API group to run before or after its own logic.", registerMethod: "registerMiddleware" },
|
|
221
221
|
{ kind: "addon", payloadKey: "addon", authorFactory: "addon", description: "A reusable read fragment that enriches a query result by joining related table data.", registerMethod: "registerAddons" },
|
|
222
222
|
{ kind: "realtime_server", payloadKey: "realtime_server", authorFactory: "realtimeServer", description: "A realtime (websocket) server: the canonical-addressed container that owns realtime channels. Off until `enabled: true`. Returns a handle with `getUrl(baseUrl)`/`getPath()` for the client's socket URL (`wss://<host>/ws/<canonical>`).", registerMethod: "registerRealtimeServers" },
|
|
@@ -311,12 +311,12 @@ var VALUE_CONSTRUCTORS = [
|
|
|
311
311
|
{ name: "c.blank", signature: '(tag: "const:<type>") => Value', description: `The editor's UNCONFIGURED value box (stored value ""), emitted by codegen for a pulled workspace \u2014 do not author it. NOT a zero or an empty collection: the engine reads "" and "0" differently, so c.blank("const:int") \u2260 c.int(0) and neither canonicalizes into the other. Constant tags except const/const:obj, whose blanks are c.text("")/c.obj(null).` },
|
|
312
312
|
{ name: "c.bool", signature: "(b: boolean) => Value", description: 'Boolean constant \u2192 tag "const:bool".' },
|
|
313
313
|
{ name: "c.null", signature: '(tag?: "const:null" | "const:obj") => Value', description: `Null constant, stored value "null". Bare c.null() is tag "const:null". c.null("const:obj") is the OBJECT-TYPED null the engine writes into a db.* statement's @meta slot \u2014 different stored bytes from c.obj(null), which is the blank object (value ""), though both evaluate to null. Codegen emits whichever spelling the workspace stored; do not swap one for the other.` },
|
|
314
|
-
{ name: "c.obj", signature: "(o?: Json | null) => Value", description: 'Object constant \u2192 tag "const:obj". A populated one stores an empty {} carrying one `set` filter per key \u2014 the editor\'s form, and the only populated form the engine reads back (a populated JSON string arrives truncated and fails the request with ERROR_FATAL "Unable to decode."). \u26A0 a ZERO-BASED numeric key is an INDEX in the engine\'s data model, so c.obj({"0":"a"}) evaluates to the list ["a"] (a non-zero-based one like {"2":\u2026} survives as a key) \u2014 that is the platform, not this encoding. No argument = the empty object {} \u2014 use this one. Explicit null = the legacy blank form the engine evaluates to null, NOT {}; it exists only so a pulled workspace round-trips, do not author it. Plain JSON literals only \u2014 a nested tagged value (inp/ref/auth/c.*) is rejected; for a computed object response use a record of values, not c.obj.' },
|
|
314
|
+
{ name: "c.obj", signature: "(o?: Json | null) => Value", description: 'Object constant \u2192 tag "const:obj". A populated one stores an empty {} carrying one `set` filter per key \u2014 the editor\'s form, and the only populated form the engine reads back (a populated JSON string arrives truncated and fails the request with ERROR_FATAL "Unable to decode."). \u26A0 a ZERO-BASED numeric key is an INDEX in the engine\'s data model, so c.obj({"0":"a"}) evaluates to the list ["a"] (a non-zero-based one like {"2":\u2026} survives as a key) \u2014 that is the platform, not this encoding. Write c.array([...]) for a list, or prefix the keys ("k0") for an object; `export()` warns. No argument = the empty object {} \u2014 use this one. Explicit null = the legacy blank form the engine evaluates to null, NOT {}; it exists only so a pulled workspace round-trips, do not author it. Plain JSON literals only \u2014 a nested tagged value (inp/ref/auth/c.*) is rejected; for a computed object response use a record of values, not c.obj.' },
|
|
315
315
|
{ name: "c.array", signature: "(a: Json[]) => Value", description: 'Array constant (JSON string) \u2192 tag "const:array". Plain JSON literals only \u2014 a nested tagged value is rejected, same as c.obj.' },
|
|
316
316
|
{ name: "c.expression", signature: "(source: string) => Value", description: `Xano Expression Engine source, passed through VERBATIM \u2192 tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). \u26A0\uFE0F NOT VALIDATED \u2014 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) \u2014 prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.` },
|
|
317
317
|
{ name: "c.expressionLegacy", signature: "(source: string) => Value", legacy: true, description: "the older `const:expr` expression form, emitted by codegen for workspaces that still hold one \u2014 author `c.expression` instead." },
|
|
318
318
|
{ name: "c.now", signature: "() => Value", description: "Current time as epoch-ms \u2014 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." },
|
|
319
|
-
{ name: "obj", signature: "(fields: Record<string, Value | nested>) => ObjValue<typeof fields>", description: 'Dynamic object value \u2192 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 \u2014 and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`.
|
|
319
|
+
{ name: "obj", signature: "(fields: Record<string, Value | nested>) => ObjValue<typeof fields>", description: 'Dynamic object value \u2192 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 \u2014 and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. A null-safe ref(path, { safe: true }) works inside an obj() member directly. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key \u2014 `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([...])` \u2014 prefer the bare form. 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 {}) \u2014 write c.null() or c.obj(). Still rejected: a filter ARGUMENT that carries its own chain or is a c.now() (a trailing | binds to the whole value, not one argument, and c.now() needs one), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags \u2014 build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.' },
|
|
320
320
|
{ name: "ref", signature: "(name: string, opts?: { safe?: boolean }) => Value", description: 'Reference a stack variable \u2192 tag "var". Pass { safe: true } for null-safe nested access \u2014 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.' },
|
|
321
321
|
{
|
|
322
322
|
name: "inp",
|
|
@@ -774,9 +774,9 @@ function renderTestsSection() {
|
|
|
774
774
|
"",
|
|
775
775
|
"The run is isolated in ways that make a correct test fail for reasons the failure message does not name.",
|
|
776
776
|
"",
|
|
777
|
-
"- The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs**
|
|
777
|
+
"- The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** (same as a unit test, above): a read of seeded row 1 fails with its own precondition message, not an empty-database error. Build fixtures INSIDE the test.",
|
|
778
778
|
'- `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 \u2014 `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` \u2014 when a call may fail. `s.function.run` raises instead; the two disagree.',
|
|
779
|
-
'- `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 \u2014 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.',
|
|
779
|
+
'- `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 \u2014 bind what the body needs inside the body. An outer one raises `Missing var entry: <name>` there, which the test reports as `to_throw` not matching (`export()` warns). `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.',
|
|
780
780
|
"- `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` \u2014 which reads as a broken auth gate on a gate that works.",
|
|
781
781
|
"- 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.",
|
|
782
782
|
"- Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call \u2014 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.",
|
|
@@ -812,13 +812,13 @@ function renderSections(m) {
|
|
|
812
812
|
"caller, `c.*` a constant \u2014 resolved at request time; JS operators over them do not",
|
|
813
813
|
"compute (see Gotchas). Requests share no memory \u2014 state persists in tables or redis.",
|
|
814
814
|
"",
|
|
815
|
-
`Coverage: object kinds ${m.coverage.objectKinds.implemented}/${m.coverage.objectKinds.total}, statement surfaces ${m.coverage.statements.implemented}/${m.coverage.statements.total}, filters ${m.coverage.filters.total} (${m.coverage.filters.typed} typed).`,
|
|
816
815
|
// The shortfall by name: an agent that cannot see WHICH kinds are missing
|
|
817
816
|
// will invent a factory for one. Reasons live in `manifest.json`.
|
|
817
|
+
`Authorable: statement surfaces ${m.coverage.statements.implemented}/${m.coverage.statements.total}, filters ${m.coverage.filters.total} (${m.coverage.filters.typed} typed).`,
|
|
818
818
|
`Not authorable here: ${m.coverage.objectKinds.unmodeled.map((k) => k.kind).join(", ")} \u2014 these cannot be authored and do not survive a pull; reasons in \`coverage.objectKinds.unmodeled\`.`,
|
|
819
819
|
"",
|
|
820
820
|
"This file is the whole always-read surface: the mental model, the deploy contract,",
|
|
821
|
-
"every gotcha, and control flow. Per-surface detail lives in the topic files listed",
|
|
821
|
+
"every cross-cutting gotcha, and control flow. Per-surface detail lives in the topic files listed",
|
|
822
822
|
"below \u2014 open the one whose condition matches the task, skip the rest. For",
|
|
823
823
|
"exhaustive per-entry detail in NEITHER \u2014 a statement's field schema with engine",
|
|
824
824
|
"defaults, a filter's full argument list, the `storedName` mapping \u2014 do a TARGETED",
|
|
@@ -903,6 +903,8 @@ function renderSections(m) {
|
|
|
903
903
|
"but nothing fails on a message no one reads, so in CI and in unattended agent builds",
|
|
904
904
|
"pass `--strict` (`emitBundle(app, { strict: true })` / `app.export({ strict: true })`):",
|
|
905
905
|
"every warning becomes a hard failure. Same bundle bytes either way.",
|
|
906
|
+
"Before calling the work done: `npm run typecheck`, `xanots export <entry> --strict`,",
|
|
907
|
+
"`xanots deploy <entry>`, then `xanots test run-all` against what deployed.",
|
|
906
908
|
"",
|
|
907
909
|
"Identity: object guids derive from `(type, name)` \u2014 a query's from `(api group,",
|
|
908
910
|
"verb, name)` \u2014 so renames change identity.",
|
|
@@ -948,38 +950,19 @@ function renderSections(m) {
|
|
|
948
950
|
"",
|
|
949
951
|
"**Frontend wiring.** `--static <dir>` injects the DEPLOYED env's backend URL as",
|
|
950
952
|
"`window.XANO_HOST` into EVERY html document in the build, before the app bundle runs,",
|
|
951
|
-
"so the frontend needs no rebuild to target an env.
|
|
952
|
-
"
|
|
953
|
-
"
|
|
954
|
-
"fallback:",
|
|
955
|
-
" const HOST = (typeof window !== 'undefined' && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;",
|
|
956
|
-
"In LOCAL DEV there is no injected global, so the fallback is what answers: set",
|
|
957
|
-
"`VITE_XANO_HOST` in a `.env.local` beside `.env.example` at the PROJECT ROOT. The",
|
|
958
|
-
"scaffold's vite config sets `envDir` there (its `root` is `frontend/`, and Vite",
|
|
959
|
-
"resolves `.env` files against `root`) \u2014 without it the var reads as undefined, the",
|
|
960
|
-
"host falls back to '', and every call 404s off the dev server.",
|
|
961
|
-
'\u26A0 It is INJECTED in bracket form \u2014 `window["XANO_HOST"]="\u2026"` \u2014 so verifying a deploy',
|
|
962
|
-
"by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the",
|
|
963
|
-
"bare `XANO_HOST` token.",
|
|
964
|
-
"\u26A0 `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /",
|
|
965
|
-
"`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.",
|
|
966
|
-
"**Displaying a stored file.** A file column comes back as `{ path, name, type, size,",
|
|
967
|
-
"meta, access, url }`. \u26A0 Do NOT use its `url`: on a tenant-scoped environment that field",
|
|
968
|
-
"addresses the instance host WITHOUT the `/tenant/<name>` segment and 404s, silently \u2014",
|
|
969
|
-
"as a broken `<img>`, with every API assertion still passing. Build the URL from `path`",
|
|
970
|
-
"instead: `fileUrl(row.avatar, HOST)` (exported from `@xanots/sdk`) returns `null` for",
|
|
971
|
-
"an absent file and is correct on an ephemeral and an instance workspace alike.",
|
|
972
|
-
"\u26A0 A static host serves these files verbatim, so everything injected is PUBLIC \u2014 base",
|
|
953
|
+
"so the frontend needs no rebuild to target an env. How to read it, local dev, verifying",
|
|
954
|
+
"the inject, and displaying a stored file: `llms/client.md`.",
|
|
955
|
+
"\u26A0 A static host serves its files verbatim, so everything injected is PUBLIC \u2014 base",
|
|
973
956
|
"URLs and publishable keys only, never secrets. Secrets go in backend env, read",
|
|
974
957
|
"server-side via `env(name)`.",
|
|
958
|
+
"\u26A0 `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /",
|
|
959
|
+
"`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.",
|
|
975
960
|
"",
|
|
976
961
|
"**Full CLI surface:** `xanots <command> --help` lists every command, flag, and",
|
|
977
962
|
"default; the shipped `manifest.json` carries the same in its `cli` array. This doc",
|
|
978
963
|
"does not duplicate it \u2014 it covers what you must know to AUTHOR a workspace.",
|
|
979
|
-
"**
|
|
980
|
-
"(`
|
|
981
|
-
"flat factory aliases (`dbAdd`, `dbQuery`, `setVar`, `mathAdd`, \u2026) are exported",
|
|
982
|
-
"and identical in output; prefer `s.*` in new code so examples stay consistent.",
|
|
964
|
+
"**Style:** reach statements through the `s` namespace (`s.db.add`, `s.math.add`, \u2026).",
|
|
965
|
+
"The flat aliases (`dbAdd`, `setVar`, \u2026) are identical in output; prefer `s.*`.",
|
|
983
966
|
""
|
|
984
967
|
);
|
|
985
968
|
lines.push(
|
|
@@ -987,53 +970,71 @@ function renderSections(m) {
|
|
|
987
970
|
"",
|
|
988
971
|
"Non-obvious authoring rules:",
|
|
989
972
|
"",
|
|
973
|
+
"### Values and references",
|
|
974
|
+
"",
|
|
990
975
|
"- **No callback builder.** Flat def-objects + `register*`, not",
|
|
991
976
|
" `workspace(w => w.table(...))`. `workspace(name)` returns a named `new Xano()`;",
|
|
992
977
|
" tables are `table({ schema: { col: f.text() } })`.",
|
|
993
|
-
"- **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a",
|
|
994
|
-
" stack variable (a value); `f.tableRef` is the column constructor.",
|
|
995
978
|
"- **Reference-helper picker:** `ref` = stack var (`as:` output), `inp` = input,",
|
|
996
979
|
' `col` = table column (in `db.query` `where`), `auth("id")` = the caller,',
|
|
997
980
|
" `c.*` = a constant. Pick by what you're pointing at.",
|
|
981
|
+
"- **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a",
|
|
982
|
+
" stack variable (a value); `f.tableRef` is the column constructor.",
|
|
998
983
|
"- **Tagged values are DATA \u2014 a JS template literal cannot compose them.** `${ref(...)}`",
|
|
999
984
|
' (or `"a" + ref(...)`) stringifies the tag object at BUILD time: ``c.text(`Hi ${ref("u.name")}`)``',
|
|
1000
985
|
" type-checks and encodes the literal text `Hi [object Object]`, served verbatim.",
|
|
1001
986
|
" `export()` warns; `--strict` fails. Compose at RUNTIME:",
|
|
1002
987
|
' `withFilters(c.text("Hi "), fl.concat(ref("u.name")))`, an `obj({...})`/record member,',
|
|
1003
988
|
" or `c.expression('\"Hi, \" ~ $var.u.name')`.",
|
|
1004
|
-
|
|
1005
|
-
"
|
|
1006
|
-
|
|
1007
|
-
"
|
|
1008
|
-
"
|
|
1009
|
-
"
|
|
1010
|
-
|
|
1011
|
-
"
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
"
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
"
|
|
1019
|
-
|
|
1020
|
-
"
|
|
1021
|
-
' `
|
|
1022
|
-
"
|
|
1023
|
-
"
|
|
1024
|
-
|
|
1025
|
-
"
|
|
1026
|
-
"
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
"
|
|
1033
|
-
|
|
1034
|
-
"
|
|
1035
|
-
"
|
|
1036
|
-
|
|
989
|
+
'- **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`',
|
|
990
|
+
" resolves ONLY against this def's own `input` block \u2014 a value produced earlier in",
|
|
991
|
+
' the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at',
|
|
992
|
+
" runtime with `Unable to locate input` (`export()` warns).",
|
|
993
|
+
"- **`get_input`/`get_raw_input` read the whole payload**, not one named input",
|
|
994
|
+
" (args are `{ as?, encoding?, excludeMiddleware? }` \u2014 no `name`). For a single",
|
|
995
|
+
' input use `inp("name")`.',
|
|
996
|
+
"- **To match every row, omit `where` \u2014 never pass a constant.** `where: c.bool(true)` is",
|
|
997
|
+
' not "no filter": a bare scalar literal states no condition and the engine reads the',
|
|
998
|
+
' resulting `context.search` as garbage, so it throws. An absent `where` is how "every',
|
|
999
|
+
' row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built',
|
|
1000
|
+
' elsewhere \u2014 `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.',
|
|
1001
|
+
"",
|
|
1002
|
+
"### Tables and columns",
|
|
1003
|
+
"",
|
|
1004
|
+
"- **System columns are auto-injected.** `id` + `created_at` are prepended to",
|
|
1005
|
+
" every table (`system: true` by default); declaring them by hand is redundant.",
|
|
1006
|
+
' `id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.',
|
|
1007
|
+
" Both are valid targets wherever a column name is accepted \u2014 `db.query` `sort`/",
|
|
1008
|
+
" `output`, a `db.get`/`edit`/`del` `fieldName`, etc. (the column-name type is",
|
|
1009
|
+
' `keyof schema | "id" | "created_at"`), and both appear in `InferRow<typeof table>`.',
|
|
1010
|
+
"- **A column named `run` is reserved.** The table deploys and reads back fine, then",
|
|
1011
|
+
" EVERY `s.db.add` into it 400s \u2014 at any column type, with or without a value \u2014 and",
|
|
1012
|
+
" the error names the column while complaining about the VALUE. Use `run_id`. Exact,",
|
|
1013
|
+
" case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.",
|
|
1014
|
+
"- **Self-referencing tables** need the bare-name form: inside `tweets`'s own",
|
|
1015
|
+
' schema, write `f.tableRef("tweets", { type: "int" })` \u2014 the `const tweets`',
|
|
1016
|
+
` handle isn't assigned yet, so the handle form throws "used before declaration".`,
|
|
1017
|
+
"- **Seed a table's starting rows with `table({ seed })`.** Rows typed against the",
|
|
1018
|
+
' schema, inline (`seed: [{ name: "\u2026" }]`) or from a file (`seedFile("./seed.json",',
|
|
1019
|
+
" import.meta.url)`). Only `deploy` ships them, and a re-deploy re-seeds cleanly. Never",
|
|
1020
|
+
" put secrets in `seed`. Row shape, `id` pinning, and the file rules: `llms/fields.md`.",
|
|
1021
|
+
'- **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**',
|
|
1022
|
+
' A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime',
|
|
1023
|
+
" with `Unable to locate var: u.password` \u2014 the column is simply absent from the row.",
|
|
1024
|
+
' Name it in the read\'s `output` to pull it: `s.db.get({ table: users, fieldName: "email",',
|
|
1025
|
+
' fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then',
|
|
1026
|
+
" `s.security.check_password`. `output` OVERRIDES column visibility \u2014 it is the only way to",
|
|
1027
|
+
" read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not",
|
|
1028
|
+
" return.",
|
|
1029
|
+
"- **Don't take a password through `input.password` on login \u2014 it double-hashes.**",
|
|
1030
|
+
" An `f.password()` column hashes on write, and `input.password` *also* hashes the",
|
|
1031
|
+
" submission on bind, so `s.security.check_password` compares two different hashes",
|
|
1032
|
+
" and a correct password always fails (`ok:false` on a found row). Take the submitted",
|
|
1033
|
+
" password as `input.text()` on both signup and login and pass the plaintext straight",
|
|
1034
|
+
" to `check_password` (which does the comparison hash itself). `export()` warns.",
|
|
1035
|
+
"",
|
|
1036
|
+
"### Reading and writing rows",
|
|
1037
|
+
"",
|
|
1037
1038
|
"- **DB reads are field-match, not `where`-expr.** `db.get`/`db.edit`/`db.del`/",
|
|
1038
1039
|
" `db.has`/`db.patch` match one field: `{ fieldName, fieldValue }` (`fieldName`",
|
|
1039
1040
|
" defaults to the PK `id`). Only `db.query` takes a `where`/`additionalWhere`",
|
|
@@ -1047,44 +1048,73 @@ function renderSections(m) {
|
|
|
1047
1048
|
' check-in \u2014 use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`',
|
|
1048
1049
|
" (a `where` array is ANDed) and branch on the result, rather than pushing the",
|
|
1049
1050
|
" check to the client.",
|
|
1050
|
-
"- **
|
|
1051
|
-
|
|
1052
|
-
"
|
|
1053
|
-
"
|
|
1054
|
-
|
|
1055
|
-
"
|
|
1056
|
-
|
|
1057
|
-
"
|
|
1058
|
-
"
|
|
1059
|
-
|
|
1060
|
-
"
|
|
1061
|
-
"
|
|
1062
|
-
"
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
"
|
|
1067
|
-
"
|
|
1068
|
-
"
|
|
1069
|
-
"
|
|
1070
|
-
"
|
|
1071
|
-
"
|
|
1072
|
-
"
|
|
1073
|
-
"
|
|
1074
|
-
"
|
|
1075
|
-
"
|
|
1076
|
-
"
|
|
1077
|
-
"
|
|
1078
|
-
"
|
|
1079
|
-
"
|
|
1080
|
-
"
|
|
1081
|
-
"
|
|
1082
|
-
"
|
|
1083
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
"
|
|
1086
|
-
|
|
1087
|
-
|
|
1051
|
+
"- **Drilling into a maybe-null `db.get` result 500s \u2014 use `ref(path, { safe: true })`.**",
|
|
1052
|
+
' `db.get` binds `null` on a no-match, but a nested `ref("owner.user_id")` raises a runtime',
|
|
1053
|
+
' "Unable to locate var" (HTTP 500) when `owner` is null \u2014 an ownership guard throws instead',
|
|
1054
|
+
" of failing cleanly. Two correct shapes:",
|
|
1055
|
+
' - Guard existence first \u2014 `s.precondition({ expr: expr(ref("owner"), "!=", c.null()),',
|
|
1056
|
+
' error_type: "notfound", error: c.text("Not found.") })` \u2014 then drill WITHOUT `safe`.',
|
|
1057
|
+
" - Or drill null-safe inside an EXPRESSION or `obj()` operand:",
|
|
1058
|
+
' `expr(ref("owner.user_id", { safe: true }), "=", auth("id"))` yields `null` (the guard',
|
|
1059
|
+
" reads `false`) instead of 500ing. Works inside `obj({...})` too \u2014 no `s.set_var` hoist needed.",
|
|
1060
|
+
" \u26A0 EXPRESSION and `obj()` operands ONLY \u2014 never a `db.*` match argument. `null` is not a",
|
|
1061
|
+
" legal `fieldValue`/`id`, so a chained get (fetch the child row, then its parent to check",
|
|
1062
|
+
" the owner) fails with HTTP 400 `Missing param: field_value` one statement BEFORE the guard.",
|
|
1063
|
+
" There guarding existence first is MANDATORY. `export()` warns on a safe ref in that position.",
|
|
1064
|
+
" \u26A0 That is for a base that EXISTS and may be null. If the base binds nothing at all (a typo),",
|
|
1065
|
+
" `{ safe: true }` buries the bug as a silent `null` \u2014 `export()` warns with a did-you-mean;",
|
|
1066
|
+
" fix the name instead.",
|
|
1067
|
+
"",
|
|
1068
|
+
"### Stacks and calls",
|
|
1069
|
+
"",
|
|
1070
|
+
"- **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/",
|
|
1071
|
+
" `s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`",
|
|
1072
|
+
" take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);",
|
|
1073
|
+
" `s.group(body)` and `s.util.post_process(body)` take it **positionally**.",
|
|
1074
|
+
" `s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the",
|
|
1075
|
+
" authored signatures in `llms/statements-data.md`.",
|
|
1076
|
+
"- **`s.api.call` / `s.task.call` / `s.trigger.call` / `s.workflow_test.call` are",
|
|
1077
|
+
" WORKFLOW-TEST ONLY.** Outside a `workflowTest({...})` stack the engine cannot reach",
|
|
1078
|
+
" the target, so one in a query/function/task deploys clean and then answers the first",
|
|
1079
|
+
" real request with `ERROR_FATAL: <Type> does not exist: <type>:<n>` \u2014 and not per",
|
|
1080
|
+
" host kind, the same call fails identically from a function a query runs.",
|
|
1081
|
+
" `export()` refuses them. `s.function.call`, `s.tool.call`, `s.middleware.call` and",
|
|
1082
|
+
" `s.addon.call` run anywhere, as does `s.function.run` (the ordinary function",
|
|
1083
|
+
" invocation). To share logic between endpoints, put it in a `defineFunction` and",
|
|
1084
|
+
" `s.function.run` it from both.",
|
|
1085
|
+
"- **`expect.*` is a unit-test assertion; `s.expect.*` is a workflow-test statement.**",
|
|
1086
|
+
" A statement's `mock` is keyed by TEST NAME; an undeclared name throws. See `llms/tests.md`.",
|
|
1087
|
+
"- **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**",
|
|
1088
|
+
" Spreading `...requireX()` where the helper is typed `Statement[]` drops the stack's TUPLE",
|
|
1089
|
+
" type, so EVERY `ref()`/`as` in that stack \u2014 even ones after the spread \u2014 resolves to",
|
|
1090
|
+
" `unknown` and the response types as `StackTupleWidened`. Nothing fails at the helper; it",
|
|
1091
|
+
" surfaces where the response is consumed, often a frontend typecheck. Fix: return",
|
|
1092
|
+
" `statements(s.a(...), s.b(...))` \u2014 its tuple survives the spread. A helper that builds",
|
|
1093
|
+
" its array in a LOOP cannot be a tuple; declare `responseShape` on the query there.",
|
|
1094
|
+
"- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**",
|
|
1095
|
+
" The regex filters (`regex_test`/`regex_match`/`regex_replace`/\u2026) are PHP `preg_*`: the",
|
|
1096
|
+
" piped value is the PATTERN and must be delimiter-wrapped, and the ARGUMENT is the subject.",
|
|
1097
|
+
' Correct: `withFilters(c.regex(/^[a-z-]+$/i), fl.regex_test(inp("slug")))`.',
|
|
1098
|
+
' - A bare `c.text("^\u2026$")` is an invalid pattern that matches *nothing* for every input,',
|
|
1099
|
+
' so a precondition on it silently rejects all values. `c.regex(body, "i")` wraps +',
|
|
1100
|
+
" escapes it for you (a JS `RegExp` too: `c.regex(/^\u2026$/i)`).",
|
|
1101
|
+
" - Reversed \u2014 subject piped, pattern in the argument \u2014 reads correctly, type-checks, and",
|
|
1102
|
+
" answers false for EVERY input, so an `if (matches) reject` guard admits what it refuses.",
|
|
1103
|
+
" - Build time refuses both: `withFilters` throws on a bare `c.text` pattern from ANY position",
|
|
1104
|
+
" in the chain (a normalizer in front of the regex filter is refused too; nothing upstream",
|
|
1105
|
+
" adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match` is the",
|
|
1106
|
+
" same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.",
|
|
1107
|
+
" `export()` warns on a reversed pair in stored bytes.",
|
|
1108
|
+
" - Better still: a native typed input (`input.email`) over hand-rolled validation.",
|
|
1109
|
+
"- **An `auth()`-keyed limiter collapses on any host with no caller identity \u2014 at every**",
|
|
1110
|
+
' **tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))) })`',
|
|
1111
|
+
" is the per-user form, but `auth()` is `null` on a public query, a task, or a function,",
|
|
1112
|
+
" and every caller then shares one bucket. Attaching it once at",
|
|
1113
|
+
" `apiGroup({ middleware })` or the workspace tier is the tempting DRY move and inherits",
|
|
1114
|
+
" the same collapse onto every member endpoint. Export warns, naming the tier.",
|
|
1115
|
+
"",
|
|
1116
|
+
"### Identity, build, and deploy",
|
|
1117
|
+
"",
|
|
1088
1118
|
"- **Same-name siblings collide \u2014 queries excepted: their identity carries group + verb.**",
|
|
1089
1119
|
" Guids derive from `(type, name)`, so two functions (or tables, toolsets, \u2026) sharing",
|
|
1090
1120
|
" a name derive ONE guid and `export()` throws \u2014 give them DISTINCT names. A QUERY",
|
|
@@ -1095,137 +1125,34 @@ function renderSections(m) {
|
|
|
1095
1125
|
" is NOT a lasting fix: a lock entry cannot hold two guids, so `export --lock`",
|
|
1096
1126
|
" refuses the pair (`export()` warns even unlocked); it is for pinning identity",
|
|
1097
1127
|
" across a rename, not for sharing a name.",
|
|
1098
|
-
"- **`export()` vs `emitBundle()` vs `writeBundle()`:** `
|
|
1099
|
-
"
|
|
1100
|
-
"
|
|
1101
|
-
"
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
"
|
|
1105
|
-
"
|
|
1106
|
-
"
|
|
1107
|
-
"
|
|
1108
|
-
"
|
|
1109
|
-
"
|
|
1110
|
-
" The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,",
|
|
1111
|
-
" the lock model) is on `@xanots/sdk/internal` and is never needed to author.",
|
|
1112
|
-
" READING a bundle back is `@xanots/sdk/bundle` \u2014 a statement walker (`2.if.0` paths),",
|
|
1113
|
-
" a structural hash, `mvp:*` catalog, `tableRefOf`.",
|
|
1114
|
-
"- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls",
|
|
1115
|
-
" no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its",
|
|
1116
|
-
" `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:",
|
|
1117
|
-
" the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.",
|
|
1118
|
-
" \u26A0 A FLOOR \u2014 **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules",
|
|
1119
|
-
" never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer",
|
|
1120
|
-
" one, adds ~2 kB \u2014 so reducing what a def does will not reduce it.",
|
|
1121
|
-
" Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) \u2014 verbs, paths, and sockets as",
|
|
1122
|
-
' plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,',
|
|
1123
|
-
' `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base',
|
|
1124
|
-
" URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.",
|
|
1128
|
+
"- **`export()` vs `emitBundle()` vs `writeBundle()`:** `export()` returns the bundle object,",
|
|
1129
|
+
" `emitBundle()` the pretty JSON string, `writeBundle(app, path)` writes it to disk. All",
|
|
1130
|
+
" three run the SAME build-time checks, INCLUDING seed validation of a literal `seed: [...]`",
|
|
1131
|
+
" array (row shape, unknown column, coercion, enum membership, the all-or-nothing `id`",
|
|
1132
|
+
' rule). A DEFERRED seed \u2014 a thunk (`() => import("./seed.json")`) or `seedFile()` \u2014 is',
|
|
1133
|
+
" checked only by the `xanots export`/`deploy` CLI path. Seed rows are never emitted into",
|
|
1134
|
+
" the bundle either way; only `deploy` ships them.",
|
|
1135
|
+
" - Entry points: the `node:fs` writers (`writeBundle`/`writeArtifact`) and lock-file I/O",
|
|
1136
|
+
" import from `@xanots/sdk/node`, NOT the browser-safe `@xanots/sdk` entry (a frontend",
|
|
1137
|
+
" imports defs from it for `getPath()`/`InferInput`). `@xanots/sdk/internal` is the",
|
|
1138
|
+
" compiler machinery, never needed to author. READING a bundle back is `@xanots/sdk/bundle`",
|
|
1139
|
+
" \u2014 a statement walker (`2.if.0` paths), a structural hash, `mvp:*` catalog, `tableRefOf`.",
|
|
1125
1140
|
"- **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not",
|
|
1126
1141
|
" extensionless \u2014 the defs compile under `moduleResolution: bundler`. Add the `.js`.",
|
|
1127
|
-
"- **Verifying a def outside a bundler.** Inside a bundler (Vite/webpack) importing a",
|
|
1128
|
-
" query def to read `getPath()`/`verb` works directly. To spot-check from Node, run a REAL",
|
|
1129
|
-
' file with `tsx <file.ts>` **from inside the project root** \u2014 not `tsx -e "import \u2026"`',
|
|
1130
|
-
" (its CJS-preparse mis-resolves the package `exports` map \u2192 ERR_PACKAGE_PATH_NOT_EXPORTED),",
|
|
1131
|
-
" and not bare `node file.ts` (chokes on the `.js`-specifier intra-workspace imports the",
|
|
1132
|
-
" xanots CLI's own loader resolves). Running from outside the project root also breaks",
|
|
1133
|
-
" the `@xanots/sdk` specifier resolution.",
|
|
1134
|
-
"- **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**",
|
|
1135
|
-
" Factoring a repeated guard into `function requireX(): Statement[]` and spreading it",
|
|
1136
|
-
' (`...requireX("d")`) drops the stack\'s TUPLE type, so EVERY `ref()`/`as` in that stack \u2014',
|
|
1137
|
-
" including ones declared after the spread \u2014 resolves to `unknown` and the query's",
|
|
1138
|
-
" response types as `StackTupleWidened`. Nothing fails at the helper: the error surfaces",
|
|
1139
|
-
" wherever the response is consumed, often a frontend package's typecheck. Fix: return",
|
|
1140
|
-
" `statements(s.a(...), s.b(...))` \u2014 a const-generic identity export whose tuple survives",
|
|
1141
|
-
" the spread. A helper that builds its array in a LOOP cannot be a tuple; declare",
|
|
1142
|
-
" `responseShape` on the query there. See `llms/statements-runtime.md`.",
|
|
1143
|
-
"- **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/",
|
|
1144
|
-
" `s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`",
|
|
1145
|
-
" take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);",
|
|
1146
|
-
" `s.group(body)` and `s.util.post_process(body)` take it **positionally**.",
|
|
1147
|
-
" `s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the",
|
|
1148
|
-
" authored signatures in `llms/statements-data.md`.",
|
|
1149
|
-
"- **MCP servers & agents are distinct root kinds** that both persist under the",
|
|
1150
|
-
" `toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes",
|
|
1151
|
-
" tools over MCP (auth is per-tool \u2014 no server-level gate); `agent({...})` carries a",
|
|
1152
|
-
" typed `llm` block \u2014 and so may an `mcpServer`, since the two are ONE stored",
|
|
1153
|
-
" object distinguished by `type`. Their `tools` take a `tool()` handle (or name), resolved to the",
|
|
1154
|
-
" tool's guid like the call family; a raw numeric `id` is an escape hatch.",
|
|
1155
|
-
"- **`task.schedule` is an array** of `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`",
|
|
1156
|
-
" (`ScheduleDef[]`), not a single `{ type, value }`. `freq` is seconds; `startsOn`/",
|
|
1157
|
-
' `endsOn` are timestamp strings validated at encode time \u2014 `"2026-01-01T00:00:00Z"`, or',
|
|
1158
|
-
' the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries. Never',
|
|
1159
|
-
" epoch numbers, and never zoneless: the engine stores the string verbatim without",
|
|
1160
|
-
" checking it, so one it cannot parse deploys clean and the task then never fires.",
|
|
1161
|
-
"- **`get_input`/`get_raw_input` read the whole payload**, not one named input",
|
|
1162
|
-
" (args are `{ as?, encoding?, excludeMiddleware? }` \u2014 no `name`). For a single",
|
|
1163
|
-
' input use `inp("name")`.',
|
|
1164
|
-
'- **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**',
|
|
1165
|
-
' A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime',
|
|
1166
|
-
" with `Unable to locate var: u.password` \u2014 the column is simply absent from the row.",
|
|
1167
|
-
' Name it in the read\'s `output` to pull it: `s.db.get({ table: users, fieldName: "email",',
|
|
1168
|
-
' fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then',
|
|
1169
|
-
" `s.security.check_password`. `output` OVERRIDES column visibility \u2014 it is the only way to",
|
|
1170
|
-
" read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not",
|
|
1171
|
-
" return.",
|
|
1172
|
-
"- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**",
|
|
1173
|
-
" The regex filters (`regex_test`/`regex_match`/`regex_replace`/\u2026) are pattern-piped",
|
|
1174
|
-
" PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped, and the",
|
|
1175
|
-
' ARGUMENT is the subject. A bare `c.text("^\u2026$")` is an invalid pattern that matches',
|
|
1176
|
-
" *nothing* for every input, so a precondition on it silently rejects all values.",
|
|
1177
|
-
' `c.regex(body, "i")` wraps + escapes it for you (a JS `RegExp` too: `c.regex(/^\u2026$/i)`).',
|
|
1178
|
-
" Reversed \u2014 subject piped, pattern in the argument \u2014 reads correctly, type-checks, and",
|
|
1179
|
-
" answers false for EVERY input, so an `if (matches) reject` guard admits what it",
|
|
1180
|
-
" refuses. `withFilters` throws on a bare `c.text` pattern from ANY position in",
|
|
1181
|
-
" the chain (a normalizer in front of the regex filter is refused too; nothing upstream",
|
|
1182
|
-
" adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match`",
|
|
1183
|
-
" is the same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.",
|
|
1184
|
-
" `export()` warns on a reversed pair in stored bytes. Better still:",
|
|
1185
|
-
" a native typed input (`input.email`) over hand-rolled validation.",
|
|
1186
|
-
"- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any",
|
|
1187
|
-
" number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one",
|
|
1188
|
-
" container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB",
|
|
1189
|
-
" folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.",
|
|
1190
|
-
'- **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`',
|
|
1191
|
-
" resolves ONLY against this def's own `input` block \u2014 a value produced earlier in",
|
|
1192
|
-
' the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at',
|
|
1193
|
-
" runtime with `Unable to locate input` (`export()` warns).",
|
|
1194
|
-
"- **Don't take a password through `input.password` on login \u2014 it double-hashes.**",
|
|
1195
|
-
" An `f.password()` column hashes on write, and `input.password` *also* hashes the",
|
|
1196
|
-
" submission on bind, so `s.security.check_password` compares two different hashes",
|
|
1197
|
-
" and a correct password always fails (`ok:false` on a found row). Take the submitted",
|
|
1198
|
-
" password as `input.text()` on both signup and login and pass the plaintext straight",
|
|
1199
|
-
" to `check_password` (which does the comparison hash itself).",
|
|
1200
1142
|
"- **Agents authenticate with env vars \u2014 never `xanots login`.** `login` blocks on a",
|
|
1201
|
-
" browser consent no agent can complete. Set
|
|
1202
|
-
"
|
|
1203
|
-
" survives repeated runs.
|
|
1204
|
-
"
|
|
1205
|
-
"
|
|
1206
|
-
|
|
1207
|
-
' "instance_base_url": \u2026, "workspace_id": <n>,
|
|
1208
|
-
"
|
|
1143
|
+
" browser consent no agent can complete. Set all three of `$XANO_INSTANCE_URL`,",
|
|
1144
|
+
" `$XANO_WORKSPACE_ID`, `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no",
|
|
1145
|
+
" rotation, so it survives repeated runs.",
|
|
1146
|
+
" - The triple outranks every other credential (`--config` and `$XANO_REFRESH_TOKEN` included,",
|
|
1147
|
+
" each named on stderr when displaced). Setting SOME of the three is a hard error, so a",
|
|
1148
|
+
" misspelled secret cannot fall through to whatever credential the runner has.",
|
|
1149
|
+
' - As a file: `{ "type": "token", "instance_base_url": \u2026, "workspace_id": <n>,',
|
|
1150
|
+
' "meta_api_token": \u2026 }`. The older `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still',
|
|
1151
|
+
" works but ROTATES: single-use.",
|
|
1209
1152
|
"- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,",
|
|
1210
1153
|
" and every trigger \u2014 `tableTrigger` included \u2014 run normally on an ephemeral env, which",
|
|
1211
1154
|
" is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron",
|
|
1212
1155
|
" cleanup, MCP tool call) by deploying it and letting it run.",
|
|
1213
|
-
'- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to',
|
|
1214
|
-
' `["a"]`: a numeric key IS an index in the engine\'s data model, so keys that are exactly',
|
|
1215
|
-
" `0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you",
|
|
1216
|
-
' want the list, or prefix the keys (`"k0"`) when you want an object. A non-zero-based',
|
|
1217
|
-
' numeric key (`{ "2": \u2026 }`) survives as a key. Export warns.',
|
|
1218
|
-
"- **To match every row, omit `where` \u2014 never pass a constant.** `where: c.bool(true)` is",
|
|
1219
|
-
' not "no filter": a bare scalar literal states no condition and the engine reads the',
|
|
1220
|
-
' resulting `context.search` as garbage, so it throws. An absent `where` is how "every',
|
|
1221
|
-
' row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built',
|
|
1222
|
-
' elsewhere \u2014 `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.',
|
|
1223
|
-
"- **An `auth()`-keyed limiter collapses on any host with no caller identity \u2014 at every**",
|
|
1224
|
-
' **tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))) })`',
|
|
1225
|
-
" is the per-user form, but `auth()` is `null` on a public query, a task, or a function,",
|
|
1226
|
-
" and every caller then shares one bucket. Attaching it once at",
|
|
1227
|
-
" `apiGroup({ middleware })` or the workspace tier is the tempting DRY move and inherits",
|
|
1228
|
-
" the same collapse onto every member endpoint. Export warns, naming the tier.",
|
|
1229
1156
|
""
|
|
1230
1157
|
);
|
|
1231
1158
|
const objectKinds = [];
|
|
@@ -1262,8 +1189,18 @@ function renderSections(m) {
|
|
|
1262
1189
|
' - `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else \u2014 most often a lowercase `"post"` \u2014 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.`',
|
|
1263
1190
|
" - `apiGroup`: an `apiGroup()` def handle (or its name) \u2014 binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.",
|
|
1264
1191
|
' - `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) \u2014 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.',
|
|
1265
|
-
|
|
1266
|
-
'
|
|
1192
|
+
" - `name` is the endpoint PATH within the group.",
|
|
1193
|
+
' - 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.',
|
|
1194
|
+
" - Every `{param}` MUST have a matching input or `query()` THROWS \u2014 Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing.",
|
|
1195
|
+
' - 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).',
|
|
1196
|
+
' - 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 \u2014 `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.',
|
|
1197
|
+
" - Inputs absent from the path are ordinary query-string/body params.",
|
|
1198
|
+
' - Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano \u2014 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.',
|
|
1199
|
+
' - **Client recipe:** `q.getPath({ params: { slug: "hello" } })` \u2192 `/api:<canonical>/blog/hello` \u2014 never interpolate by hand.',
|
|
1200
|
+
" - `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 \u2014 `%2e` counts \u2014 addressing a different endpoint).",
|
|
1201
|
+
" - The keys are typed from the literal `name`, so a typo is a compile error.",
|
|
1202
|
+
" - 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.",
|
|
1203
|
+
" - What importing a def costs a browser bundle, the `routes.gen.ts` alternative, and how to spot-check a def from Node: `llms/client.md`.",
|
|
1267
1204
|
"- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` \u2014 a query container; register it and bind queries to it via their `apiGroup`.",
|
|
1268
1205
|
" - `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.",
|
|
1269
1206
|
' - `mode?`: `"default"` (the default) | `"custom"` | `"disabled"`, lowercase \u2014 a fourth value THROWS at export (`apiGroup()` itself does not check), because Xano neither rejects nor blanks it: it DROPS THE WHOLE API GROUP on import, so the deploy succeeds and every query in the group 404s. \u26A0 Every OTHER field applies only under `"custom"`: `"default"` serves a FIXED permissive policy (any origin, `allow-headers: *`, `allow-credentials: true`, `max-age: 86400`) and ignores the block, so setting `maxAge`/`allowCredentials`/`allowHeaders` alone changes nothing. `"disabled"` sends no CORS headers at all, so every browser call fails.',
|
|
@@ -1315,6 +1252,10 @@ function renderSections(m) {
|
|
|
1315
1252
|
" `ambiguous-condition`.",
|
|
1316
1253
|
"- A **filtered** operand (`withFilters(...)`) works inline in any condition/`where` (conditional,",
|
|
1317
1254
|
' while, `db.query`/addon, \u2026) \u2014 e.g. `cmp(withFilters(col("title"), fl.trim()), "=", inp("q"))`.',
|
|
1255
|
+
"- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any",
|
|
1256
|
+
" number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one",
|
|
1257
|
+
" container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB",
|
|
1258
|
+
" folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.",
|
|
1318
1259
|
'- e.g. `db.query({ table: posts, where: expr(col("author"), "=", auth("id")), as: "rows" })`.',
|
|
1319
1260
|
""
|
|
1320
1261
|
];
|
|
@@ -1336,6 +1277,79 @@ function renderSections(m) {
|
|
|
1336
1277
|
'- \u26A0 `mode: "always"` on a long body spends its whole length on every request, and nothing in the types says so. `"auto"` is the default for that reason. For an `auto` item `description` is ALL the agent sees until it decides to load the body, so write it to be matched against a request rather than as a title.',
|
|
1337
1278
|
"- `name` takes letters, digits, and `/ _ - { } . ` or a space; anything else is an export error."
|
|
1338
1279
|
];
|
|
1280
|
+
const client = [
|
|
1281
|
+
"Importing a def into a browser bundle or a Node script \u2014 for `getPath()`, `getUrl()`, `verb`, or `InferResponse` \u2014 runs its factory calls; these are the costs and the checks.",
|
|
1282
|
+
"",
|
|
1283
|
+
"- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls",
|
|
1284
|
+
" no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its",
|
|
1285
|
+
" `getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:",
|
|
1286
|
+
" the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.",
|
|
1287
|
+
" \u26A0 A FLOOR \u2014 **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules",
|
|
1288
|
+
" never removes it. The floor is the RUNTIME, not the def: a second or much richer def",
|
|
1289
|
+
" adds ~2 kB, so trimming a def does not shrink it.",
|
|
1290
|
+
" Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) \u2014 verbs, paths, and sockets as",
|
|
1291
|
+
' plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`',
|
|
1292
|
+
' `channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base',
|
|
1293
|
+
" URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.",
|
|
1294
|
+
"- **Verifying a def outside a bundler.** Inside a bundler (Vite/webpack) importing a",
|
|
1295
|
+
" query def to read `getPath()`/`verb` works directly. To spot-check from Node, run a REAL",
|
|
1296
|
+
' file with `tsx <file.ts>` **from inside the project root** \u2014 not `tsx -e "import \u2026"`',
|
|
1297
|
+
" (its CJS-preparse mis-resolves the package `exports` map \u2192 ERR_PACKAGE_PATH_NOT_EXPORTED),",
|
|
1298
|
+
" and not bare `node file.ts` (chokes on the `.js`-specifier intra-workspace imports the",
|
|
1299
|
+
" xanots CLI's own loader resolves). Running from outside the project root also breaks",
|
|
1300
|
+
" the `@xanots/sdk` specifier resolution.",
|
|
1301
|
+
"",
|
|
1302
|
+
"**Reading the injected backend URL.** `xanots deploy <entry> --static <dir>` writes the deployed env's URL",
|
|
1303
|
+
"into every html document: a prerendered build serves a different one per",
|
|
1304
|
+
"route, and a route without the global renders fine while every call goes to the wrong",
|
|
1305
|
+
"origin. Read it at runtime with a build-time fallback:",
|
|
1306
|
+
" const HOST = (typeof window !== 'undefined' && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;",
|
|
1307
|
+
"In LOCAL DEV there is no injected global, so the fallback is what answers: set",
|
|
1308
|
+
"`VITE_XANO_HOST` in a `.env.local` beside `.env.example` at the PROJECT ROOT. The",
|
|
1309
|
+
"scaffold's vite config sets `envDir` there (its `root` is `frontend/`, and Vite",
|
|
1310
|
+
"resolves `.env` files against `root`) \u2014 without it the var reads as undefined, the",
|
|
1311
|
+
"host falls back to '', and every call 404s off the dev server.",
|
|
1312
|
+
'\u26A0 It is INJECTED in bracket form \u2014 `window["XANO_HOST"]="\u2026"` \u2014 so verifying a deploy',
|
|
1313
|
+
"by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the",
|
|
1314
|
+
"bare `XANO_HOST` token.",
|
|
1315
|
+
"**Displaying a stored file.** A file column comes back as `{ path, name, type, size,",
|
|
1316
|
+
"meta, access, url }`. \u26A0 Do NOT use its `url`: on a tenant-scoped environment that field",
|
|
1317
|
+
"addresses the instance host WITHOUT the `/tenant/<name>` segment and 404s, silently \u2014",
|
|
1318
|
+
"as a broken `<img>`, with every API assertion still passing. Build the URL from `path`",
|
|
1319
|
+
"instead: `fileUrl(row.avatar, HOST)` returns `null` for",
|
|
1320
|
+
"an absent file and is correct on an ephemeral and an instance workspace alike."
|
|
1321
|
+
];
|
|
1322
|
+
const errors = [
|
|
1323
|
+
"### Request-time (deployed) failures",
|
|
1324
|
+
"",
|
|
1325
|
+
'- `Unable to locate input: <name>` \u2014 `inp()` names an undeclared input, or an attached middleware reads its own `input` (never bound). Declare it; a stack value is `ref("var.field")`; middleware reads the body with `s.util.get_all_input`. `llms.txt`; `llms/kinds-core.md`.',
|
|
1326
|
+
"- `Unable to locate var: <a.b>` (HTTP 500) \u2014 a dotted `ref` into a null base (a `db.get` miss), or a column absent from the row (`f.password` is `internal`). Guard existence first, or use the null-safe `safe` option; name the column in `output`. `llms.txt`.",
|
|
1327
|
+
"- `Missing var entry: <name>` inside `s.expect.to_throw` \u2014 the body runs in an isolated var stack; bind what it needs inside the body. `llms/tests.md`.",
|
|
1328
|
+
"- `Missing param: field_value` (HTTP 400) \u2014 a `db.*` match argument resolved to `null` (a safe ref, a nullable foreign key). Guard existence before the lookup; an optional FK stores `0` and is read with field-match `db.get`. `llms.txt`; `llms/fields.md`.",
|
|
1329
|
+
"- `Value is less than the minimum value of 1` (HTTP 400) \u2014 `db.get_by_id` given `0`; use field-match `db.get`. `llms/statements-data.md`.",
|
|
1330
|
+
"- `Invalid op: <op>` \u2014 a `cmp` operator (`in`, `like`, \u2026) in a RUNTIME condition, which takes the `expr` set only; write `or(expr(...), expr(...))`. `llms/kinds-core.md`.",
|
|
1331
|
+
"- `Unsupported param format` / `Unsupported parameter reference` \u2014 an aggregate or `eval` `name` that is not a bare column, or a bare `eval` name for a joined column. `llms/statements-data.md`.",
|
|
1332
|
+
"- `ParseError: Invalid value for param` (HTTP 400) \u2014 `contains`/`@>`/`overlaps` on a text column (use `includes`), or a joined column qualified without `tableAlias`. `llms/statements-data.md`.",
|
|
1333
|
+
"- `ERROR_FATAL: <Type> does not exist: <type>:<n>` \u2014 `s.api.call`/`s.task.call`/`s.trigger.call`/`s.workflow_test.call` outside a workflow test; share logic via `defineFunction` + `s.function.run`. `llms.txt`.",
|
|
1334
|
+
'- `ERROR_FATAL "Unable to decode."` \u2014 a populated JSON string where the engine expects `c.obj`\'s form; write `c.obj({...})` or `obj({...})`. `llms/values.md`.',
|
|
1335
|
+
'- `Precondition failed.` in place of your message \u2014 `error` was a bare string; pass `c.text("\u2026")`. `llms/fields.md`.',
|
|
1336
|
+
"- `Param: token - Text filter requires an integer, float, string or boolean value` \u2014 `s.api.call` `auth.token` given a tagged `Value`; it must be a bare string. `llms/statements-calls.md`.",
|
|
1337
|
+
"- Every `s.db.add` into one table 400s naming a column while complaining about its VALUE \u2014 the column is named `run`. `llms.txt`.",
|
|
1338
|
+
'- HTTP 200 with an EMPTY body where `0` was expected \u2014 a bare `returnType: "count"` of zero; wrap it as `{ count: ref("n") }`. `llms/statements-data.md`.',
|
|
1339
|
+
'- A lambda returns error TEXT with HTTP 200 (`Could not resolve "node:crypto"`, an undefined-binding message) \u2014 the body threw; use the preloaded globals, the surface\'s bindings, and `capture`. `llms/lambda.md`.',
|
|
1340
|
+
"- `Unable to locate request.` (404) on an endpoint that deployed \u2014 a lowercase verb, a `.` in the name, or a CORS `mode` typo that dropped the whole API group. `llms/kinds-core.md`.",
|
|
1341
|
+
'- A browser call fails on a missing `access-control-allow-origin` while deploy looks fine \u2014 CORS `mode: "custom"` with `"*"` or an unmatched origin. `llms/kinds-core.md`.',
|
|
1342
|
+
"- A websocket is closed right after the handshake \u2014 a refused `connect` gate: an empty/falsy return, a crash, or a gating trigger with no `response`. `llms/triggers.md`.",
|
|
1343
|
+
"- `to_throw failed - response is ok` around an auth-refused call \u2014 `ERROR_CODE_ACCESS_DENIED` carries no message; the auth gate is not reachable from a workflow test. `llms/tests.md`.",
|
|
1344
|
+
"- Output reads `Hi [object Object]` \u2014 a tagged value inside a JS template literal; compose at runtime with `withFilters` + `fl.concat`. `llms.txt`.",
|
|
1345
|
+
"",
|
|
1346
|
+
"### Build-time and tooling failures",
|
|
1347
|
+
"",
|
|
1348
|
+
'- `must be ES modules` \u2014 package.json says `"type": "commonjs"`; set `"type": "module"`. `llms.txt` Quickstart.',
|
|
1349
|
+
"- A response types as `StackTupleWidened` \u2014 a `Statement[]` helper was spread into the stack; return `statements(...)`. `llms.txt`; `llms/statements-runtime.md`.",
|
|
1350
|
+
'- `ERR_PACKAGE_PATH_NOT_EXPORTED` \u2014 `tsx -e "import \u2026"`, or running from outside the project root; run a real file with `tsx <file.ts>` from the root. `llms/client.md`.',
|
|
1351
|
+
"- `Missing statement: mvp:placeholder` on import \u2014 an unconfigured slot in a pulled tree; replace it with the statement it stands in for. `llms/legacy.md`."
|
|
1352
|
+
];
|
|
1339
1353
|
const kindsAgentMcp = [
|
|
1340
1354
|
"- `mcpServer({ name, guid?, description?, instructions?, docs?, enabled?, canonical?, spec?, tags?, history?, tools?, llm?, output? })` \u2014 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 \u2014 `getUrl` is NOT idempotent, a `baseUrl` already carrying an `/x2/mcp/<\u2026>/stream` path THROWS, so resolve ONCE from the instance base URL. Fires on an ephemeral.",
|
|
1341
1355
|
" - `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) \u2014 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.",
|
|
@@ -1344,7 +1358,8 @@ function renderSections(m) {
|
|
|
1344
1358
|
" - `tools?`: same `ToolsetToolEntry[]` as `mcpServer` \u2014 bare `tool()` handles, or a `{ tool, enabled?, auth? }` wrapper when one needs `enabled`/`auth`.",
|
|
1345
1359
|
' - `output?`: `{ schema: Record<string, input.*>, enabled? }` \u2014 structured-output schema. `schema` is a named-field record authored with the `input.*` catalog, exactly like a `defineFunction`/`query` `input:` map (the stored `structuredOutputsSchema` is the same wire shape as function inputs). e.g. `output: { schema: { priority: input.enum(["low","high"]), summary: input.text() } }`. When you pass the agent *handle* to `s.ai.agent.run({ agent })`, `.result` is typed straight from this schema \u2014 no `resultShape` witness needed (the shape is declared once). `resultShape` remains only to override that, or to type an agent referenced by bare name.',
|
|
1346
1360
|
' - **Run + read recipe (`s.ai.agent.run`):** bind the run to a var (`s.ai.agent.run({ agent, args, as: "run" })`) \u2014 it produces a rich envelope, and the completion is at **`.result`**. Read one structured field with a dotted ref (`response: ref("run.result.priority")`) or the whole completion (`ref("run.result")`, typed from `output.schema`); persist it in a later step the same way (`s.set_var({ name, value: ref("run.result.summary") })`). `args` is a plain object of run inputs (`{ topic: inp("topic") }` \u2014 raw literals are fine, e.g. `{ max_steps: 3 }`) surfaced to the agent as `{{ $args.topic }}`. Tool-call / per-step data (when tools ran) rides `.toolCalls` / `.steps` on the same envelope \u2014 both optional, absent or empty when no tools executed.',
|
|
1347
|
-
" - **Marketplace:** for chat history, use `@xanots/chatbot`; for vector RAG, use `@xanots/vector` (`tools: [vector.searchTool]`). Find add-ons with `xanots marketplace search <q>`."
|
|
1361
|
+
" - **Marketplace:** for chat history, use `@xanots/chatbot`; for vector RAG, use `@xanots/vector` (`tools: [vector.searchTool]`). Find add-ons with `xanots marketplace search <q>`.",
|
|
1362
|
+
"- Both kinds persist under the `toolset` payload key, so a same-name `agent` + `mcpServer` pair derives ONE guid and `export()` throws \u2014 give them distinct names."
|
|
1348
1363
|
];
|
|
1349
1364
|
const kindsRealtime = [
|
|
1350
1365
|
"- **Realtime** \u2014 the only three-level chain: `realtimeServer` owns `realtimeChannel`s, which own `realtimeMessage` handlers. Pass the HANDLE, not a name (a channel path is unique only within its server).",
|
|
@@ -1365,11 +1380,11 @@ function renderSections(m) {
|
|
|
1365
1380
|
' - `deliverTo?`: `"channel"` (default) | `"sender"` | `"others"` | `"explicit"`. \u26A0 `"explicit"` still delivers to NOBODY \u2014 nothing selects recipients from inside a handler, and `s.realtime.publish` (which originates an event INTO a channel) is not a substitute.',
|
|
1366
1381
|
' - Only `"channel"`/`"others"` fan out AND are written to the `conversation` transcript \u2014 a `"sender"` response is invisible to every future joiner.',
|
|
1367
1382
|
' - **Both input surfaces read as ordinary inputs:** `inp("body")` for a payload field, `inp("room_id")` for the channel\'s `{room_id}`. No session lookup, no frame parsing.',
|
|
1368
|
-
" - A path param is bound ONCE at join and read from the connection thereafter, never from the frame \u2014 a sender cannot claim a room it did not join.
|
|
1383
|
+
" - A path param is bound ONCE at join and read from the connection thereafter, never from the frame \u2014 a sender cannot claim a room it did not join.",
|
|
1369
1384
|
" - `s.realtime.get_session({ as })` \u2014 the CALLER's realtime session for the current frame. FLAT shape:",
|
|
1370
1385
|
' - `authenticated` bool \xB7 `client_id` text (the AUTHED ROW ID as text, `""` anonymous) \xB7 `dbo_id` int (the auth TABLE\'s id \u2014 NOT the user\'s row id; `0` anonymous \u2014 to look the caller up use `client_id`. `dbo_id` is an int in the same position and typechecks, so a gate that keys on it finds no user and refuses EVERYONE) \xB7 `socket_id` int (transport id) \xB7 `channel` text (resolved path, `""` in a server trigger) \xB7 `params` object (bound path params, `{}` when none \u2014 `ref("session.params.room_id")`) \xB7 `extras` object \xB7 `opened_at` decimal.',
|
|
1371
1386
|
" - Works in a realtime MESSAGE stack and in CHANNEL and SERVER trigger stacks; off that path it degrades to an anonymous session.",
|
|
1372
|
-
' - For a path param prefer `inp("room_id")
|
|
1387
|
+
' - For a path param prefer `inp("room_id")` in a MESSAGE; a lifecycle TRIGGER has only the session. Reach for the session when you need the CONNECTION (identity/extras) \u2014 "who is this sender" on an anonymous-client channel.',
|
|
1373
1388
|
" - \u26A0 THREE UNRELATED THINGS ARE CALLED A CLIENT ID: `session.client_id` (app-facing identity), `session.socket_id` (transport), and a frame's `options.client_id` (the at_least_once CURSOR handle). Conflating the first and last breaks at_least_once for anonymous clients.",
|
|
1374
1389
|
" - `s.realtime.publish({ server, channel, data, message?, authTable?, authId? })` \u2014 the PUSH direction: originate a server-authored event onto a channel from ANY stack, no client frame first.",
|
|
1375
1390
|
" - `server` is the handle or its NAME (resolved by name, not guid); `channel` is the FILLED-IN path (`channel.getChannel({ room_id: 42 })`), never the template \u2014 a constant still carrying `{param}` THROWS at author time, and a constant `server`/`channel` naming nothing this workspace registers WARNS at export.",
|
|
@@ -1404,7 +1419,7 @@ function renderSections(m) {
|
|
|
1404
1419
|
" - **Conversation frames \u2014 the transcript hydrates the client, so DO NOT build a hydration endpoint.**",
|
|
1405
1420
|
' - On a `conversation` channel the replay is PUSHED automatically at join, unasked: `conversation_start` (`payload.count`) \u2192 the last `limit` messages, each a normal `action: "message"` frame carrying its ORIGINAL `type` and `payload` plus `conversation: true` and the original `ts` \u2192 `conversation_end`.',
|
|
1406
1421
|
" - So the client needs NO fetch, no `GET /messages`, and no table read to paint the initial view. Render `message` frames identically either way; the backfill paints itself.",
|
|
1407
|
-
" - \u26A0 `{ enabled: true }`
|
|
1422
|
+
" - \u26A0 `{ enabled: true }` alone is a no-op: `limit` defaults to 0 and 0 means RETAIN NONE (see `conversation` above) \u2014 always pass `limit`.",
|
|
1408
1423
|
" - The POST-HANDLER broadcast payload IS the stored transcript row \u2014 a handler must broadcast everything the UI needs to render a past message (author name, id, `created_at`). Nothing else is replayed.",
|
|
1409
1424
|
' - Only `deliverTo` `"channel"`/`"others"` are RECORDED, so a `"sender"` response is invisible to every future joiner by construction.',
|
|
1410
1425
|
" - The transcript is a capped ring (`limit`, `ttl`), not storage. Persist to a table only for durability, search, or reads BEYOND that window \u2014 never merely to hydrate a joiner.",
|
|
@@ -1437,8 +1452,20 @@ function renderSections(m) {
|
|
|
1437
1452
|
"errorTrigger}({ name, guid?, description?, active?, tags?, ... })`.",
|
|
1438
1453
|
"",
|
|
1439
1454
|
'- `tableTrigger({ name, table?, datasources?, actions?: {insert?,update?,delete?,truncate?}, stack })` \u2014 database/table trigger. `t.new` / `t.old` are the row **after** / **before** the change; `t.action` (`insert|update|delete|truncate`), `t.datasource`. Bind `table` to a `table()` handle and `t.new("col")` / `t.old("col")` are typed to that row (misspelled column = compile error). Nullability follows the enabled actions: insert \u2192 `old` is null, delete \u2192 `new` is null, update \u2192 both, truncate \u2192 neither. Config-only (no response).',
|
|
1440
|
-
"- `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` \u2014 realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name).
|
|
1441
|
-
|
|
1455
|
+
"- `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` \u2014 realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name).",
|
|
1456
|
+
" - `connect` GATES the connection \u2014 a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` below (EMPTY/FALSY DENIES \u2014 INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client).",
|
|
1457
|
+
" - A CRASH DENIES too \u2014 a gate that cannot answer must not admit. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises \u2192 everyone refused), not a breach.",
|
|
1458
|
+
" - Gating is OPT-IN: a server with no `connect` trigger accepts every connection.",
|
|
1459
|
+
" - `disconnect` is OBSERVATIONAL (return ignored, throws swallowed \u2014 cleanup must always complete).",
|
|
1460
|
+
" - Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.",
|
|
1461
|
+
"- `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` \u2014 realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.payload`, `t.client`. Bind `channel` to a `realtimeChannel()` handle \u2014 a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return:",
|
|
1462
|
+
' - `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) \u2014 return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through \u2014 or a gating trigger with NO `response` \u2014 refuses everyone, and a CRASH DENIES too. ONCE the object carries an `allowed` key admission needs STRICTLY `true` \u2014 a computed `1`/`"yes"` there DENIES. That is the inverse of a crashing message, which still delivers, and of `deliver` below.',
|
|
1463
|
+
' - A lifecycle trigger\'s inputs are PINNED to those four, so a channel PATH PARAM is NOT among them \u2014 `inp("room_id")` RAISES, which crashes the gate and so REFUSES every client; take the param from `s.realtime.get_session` (`ref("session.params.room_id")`).',
|
|
1464
|
+
' - A gate establishes NO auth, so `ref("auth.id")` reads 0 even when authenticated \u2014 identity is `t.client("permissions.dbo_id")` or the session. A SERVER connect/disconnect has no channel, so no params at all.',
|
|
1465
|
+
" - `leave` is OBSERVATIONAL (return ignored, throws swallowed).",
|
|
1466
|
+
" - `deliver` GATES delivery PER RECIPIENT \u2014 the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all \u2014 BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone.",
|
|
1467
|
+
" - **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE \u2014 INCLUDING `false`, `0`, `\"\"` \u2014 DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress \u2014 return null instead.",
|
|
1468
|
+
' - The delivered payload arrives NESTED, so read `t.payload("<field>")`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.',
|
|
1442
1469
|
'- `mcpServerTrigger({ name, mcpServer, stack?, response?, responseShape? })` / `agentTrigger({ name, agent, stack?, response?, responseShape? })` \u2014 toolset connection. Bind with the `mcpServer()`/`agent()` def handle (or its name) \u2014 it resolves to the toolset guid at export. Raw numeric `objId` is the escape hatch, rarely right: ids are assigned at import, so a handle passed to `objId` is a type error, and binding nothing deploys a trigger that never fires. Inputs: `t.toolset` (`t.toolset("name")`), `t.tools`. Response-bearing; the default stack copies `toolset`/`tools` into vars and returns them.',
|
|
1443
1470
|
"- `workspaceTrigger({ name, actions?: {branch_live?,branch_merge?,branch_new?}, stack? })` \u2014 branch lifecycle. Inputs: `t.to_branch`, `t.from_branch`, `t.action`. Config-only.",
|
|
1444
1471
|
'- `errorTrigger({ name, stack? })` \u2014 error-signature trigger. Inputs: `t.event` (`new|regression|fixed`), `t.id`, `t.signature`, `t.error` (`t.error("code")`/`t.error("message")`), `t.caller`, `t.statement`, `t.actor`, `t.count`, `t.first_seen`, `t.last_seen`, `t.fixed_at`. Config-only.',
|
|
@@ -1498,6 +1525,31 @@ function renderSections(m) {
|
|
|
1498
1525
|
"queries it.",
|
|
1499
1526
|
"`{ array: true }` makes any `f.*` scalar a **list column** \u2014 `f.text({ array: true })`",
|
|
1500
1527
|
"surfaces as `string[]` in `InferRow<typeof table>` (the column analogue of `input.list`).",
|
|
1528
|
+
"**Seed a table's starting rows with `table({ seed })`.** `seed` takes rows",
|
|
1529
|
+
"typed against the table's schema as a WRITE shape (a column without",
|
|
1530
|
+
"`required: true`, and the system columns, may be omitted; `null` needs",
|
|
1531
|
+
'`nullable: true`) \u2014 inline (`seed: [{ name: "\u2026" }]`), a FILE',
|
|
1532
|
+
'(`seed: seedFile("./seed.json", import.meta.url)`; path resolves against the DECLARING',
|
|
1533
|
+
'file), or a thunk (`seed: () => import("./seed.json")`, async ok, `.default` unwrapped).',
|
|
1534
|
+
"Inline rows are TYPED against the schema at compile time; a `seedFile`/thunk seed is",
|
|
1535
|
+
"NOT \u2014 the JSON is opaque to the typechecker, and the `xanots export`/`deploy` CLI",
|
|
1536
|
+
"validates it instead, naming the row index, the offending column, and the known",
|
|
1537
|
+
"columns. \u26A0 Prefer `seedFile` for a file: a thunk's `import()` sits in",
|
|
1538
|
+
"YOUR module, so a bundler emits the JSON as a served chunk. NEITHER is a secrecy",
|
|
1539
|
+
"boundary \u2014 a bundler that inlines the asset ships them either way. Never put secrets",
|
|
1540
|
+
"in `seed`. `deploy --static` REFUSES a build carrying internal/sensitive seed values;",
|
|
1541
|
+
"`--allow-seed-in-static` overrides for deliberately public demo credentials.",
|
|
1542
|
+
"Deploy is a full replace, so re-deploying re-seeds",
|
|
1543
|
+
"cleanly (no duplication). Omit `id` and rows auto-number `1..N` (int PK) or take",
|
|
1544
|
+
"a stable derived uuid (uuid PK); supplying `id` pins it (engine preserves it,",
|
|
1545
|
+
"resets an int sequence past the max). All-or-nothing \u2014 mixing explicit and",
|
|
1546
|
+
"omitted `id` throws. A `system:false` PK is the author's to supply. Pinning is",
|
|
1547
|
+
"`seed`-only \u2014 `s.db.bulk.add` DROPS `id` unless `allowIdField: true`.",
|
|
1548
|
+
"**`use_xdo` storage mode.** Workspace setting (`registerWorkspace({ use_xdo })`,",
|
|
1549
|
+
"default `false`) controlling whether fields are stored as JSON under the `xdo`",
|
|
1550
|
+
"column (`true`, adds a `gin(xdo)` index) or as real columns (`false`, no gin).",
|
|
1551
|
+
"Tables inherit it; override per-table with `table({ useXdo })`. Resolved at",
|
|
1552
|
+
"`export()`, so the workspace and tables can be registered in any order.",
|
|
1501
1553
|
"A **column `default` must stay within the BMP** \u2014 a 4-byte character (codepoint > U+FFFF,",
|
|
1502
1554
|
"e.g. an emoji) is mangled into invalid UTF-8 by the engine's default pipeline and is rejected",
|
|
1503
1555
|
"at export rather than 500ing at deploy (Postgres `22021`); BMP defaults (accents, `\u20AC`, most",
|
|
@@ -1586,14 +1638,10 @@ function renderSections(m) {
|
|
|
1586
1638
|
"database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHERE \u2026`),",
|
|
1587
1639
|
"which the DB applies atomically. Reserve the pipeline form for low-contention counters",
|
|
1588
1640
|
"where a rare lost update is acceptable.",
|
|
1589
|
-
"\u26A0 `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface",
|
|
1590
|
-
"
|
|
1591
|
-
"
|
|
1592
|
-
"
|
|
1593
|
-
"created afresh and the id in its name moves each time, on the same unchanged project. So",
|
|
1594
|
-
"the safe counter drops out of the typed surface: resolve the physical name from",
|
|
1595
|
-
"`information_schema` inside the request that uses it, and never store, cache or hardcode",
|
|
1596
|
-
"one. A typed atomic path needs an engine change.",
|
|
1641
|
+
"\u26A0 `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface does NOT",
|
|
1642
|
+
"expose and which is NOT STABLE across deploys \u2014 resolve it from `information_schema` inside",
|
|
1643
|
+
"the request that uses it, never store or hardcode one (rules under `s.db.direct_query` in",
|
|
1644
|
+
"`llms/statements-data.md`). A typed atomic path needs an engine change.",
|
|
1597
1645
|
""
|
|
1598
1646
|
);
|
|
1599
1647
|
const argFilters = m.filters.filter((fl) => fl.args?.length);
|
|
@@ -1754,12 +1802,27 @@ function renderSections(m) {
|
|
|
1754
1802
|
" - Attaching a typed `addon({ table, output })` handle merges its alias (the last `as` segment) onto the row in `InferResponse`: `{cols}` for `single`, `{cols}[]` for `list`, `number` for `count`, `boolean` for `exists`, and for `aggregate` an array keyed by the `group`/`eval` aliases (`unknown` values; `unknown` when neither is declared).",
|
|
1755
1803
|
" - An attachment-level `output` narrows an object/array graft further. A bare-NAME reference grafts `unknown` \u2014 narrow it at the call site.",
|
|
1756
1804
|
" - \u26A0 An alias that shadows an existing column on the queried table throws at build time; rename with a `_` prefix.",
|
|
1757
|
-
"- **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack.
|
|
1758
|
-
|
|
1759
|
-
|
|
1805
|
+
"- **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack. Distinct from `s.middleware.call` (inline invoke).",
|
|
1806
|
+
" - Attach with the host's `middleware: { pre, post }` field on `query`/`function`/`task`/`tool`/`apiGroup` (NOT triggers): each phase is an ordered list of middleware refs (def handle or name), or `{ middleware, active: false }` to keep an entry disabled.",
|
|
1807
|
+
" - Providing a phase **overrides** it (sets the stored `pre_customize`/`post_customize` flag); omitting a phase **inherits** the parent tier's chain \u2014 the engine resolves Query \u2192 API Group \u2192 Workspace at request time (override, not merge; the API-Group tier applies to queries \u2014 functions/tasks/tools have no API-group binding and inherit straight from the workspace).",
|
|
1808
|
+
" - Prefer a def handle over a bare name when the middleware pins an explicit `guid`.",
|
|
1809
|
+
" - `pre: middleware.clear()` (an empty list) overrides with nothing \u2014 stop inheriting.",
|
|
1810
|
+
" - Workspace-level defaults are the terminal tier: `workspaceConfig({ middleware: { query: { pre }, function, task, tool } })` emits the flat `{host}_{phase}` map (no `_customize` flags) \u2014 setting it replaces the whole workspace map, so unlisted hosts are cleared; omit the field to leave existing workspace middleware untouched.",
|
|
1811
|
+
"- **Middleware request context.** A `pre` middleware runs **after** auth resolution, so `auth()` is available inside the middleware when the host is authenticated (its `auth` names an auth table); on a public host `auth()` is `null`.",
|
|
1812
|
+
' - This matters for the canonical use \u2014 a rate limit keyed by `auth("id")`: on an authenticated endpoint the bucket is per-user, but attach the same middleware to a public endpoint and every anonymous caller keys under the same `null` id (one shared bucket), silently.',
|
|
1813
|
+
" - To catch that, `export()` **warns** (never blocks) when a middleware whose stack references `auth()` is directly attached to a host where `auth()` may be null \u2014 a `query` with no auth table, a `task` (scheduled, never authenticated), or a `function`/`tool` (whose auth is caller-dependent). An authenticated query (its own `auth` table set) is skipped.",
|
|
1814
|
+
" - The check is direct-attachment only; a middleware reaching a public query via API-group/workspace tier inheritance is not caught.",
|
|
1815
|
+
'- **Rate-limit recipe (the canonical middleware).** Per-user rate limiting is the most common middleware. Author it with `s.redis.ratelimit` and a **composite key** built via the filter chain \u2014 `"prefix" + auth("id")` does not exist, you build the key: `middleware({ name: "write_rl", exceptionPolicy: "rethrow", stack: [ s.redis.ratelimit({ key: withFilters(c.text("rl:write:"), fl.concat(auth("id"))), max: c.int(10), ttl: c.int(30), error: c.text("Too fast.") }) ] })`.',
|
|
1816
|
+
' - `exceptionPolicy` defaults to `"rethrow"`, which is what makes a tripped limit abort with HTTP 429; `"silent"` would let the over-limit request through.',
|
|
1817
|
+
' - Attach it with `middleware: { pre: [writeRl] }` on an **authenticated** host (its `auth` set) so `auth("id")` keys per-user; on a public host `auth("id")` is null and every caller shares one bucket (`export()` warns \u2014 see request context above).',
|
|
1818
|
+
" - **Shared-bucket rule:** co-attaching one middleware object to N hosts means all N share the *same* key \u21D2 *one* counter \u2014 `max: 10` is a global per-user budget across them, not 10-per-host. Vary the key (fold in the host/action name) for an independent limit per host.",
|
|
1760
1819
|
'- **Middleware `exceptionPolicy`** governs what a **throw** in the middleware stack does to the request (XanoTS passes the value through; the Xano engine interprets it). `"rethrow"` is the **default** \u2014 the throw aborts the request and surfaces the authored `error`/status (a tripped `s.redis.ratelimit` \u2192 HTTP 429); the `post` chain still runs. `"silent"` swallows the throw, so a guard set to it is **not enforced** \u2014 advisory middleware only. `"critical"` is `"rethrow"` plus skipping the `post` chain. The only difference between `rethrow` and `critical` is whether `post` runs \u2014 no status or logging change.',
|
|
1761
1820
|
"",
|
|
1762
|
-
'- **Request history** controls per-object execution capture (the request/task/trigger debugger). Authored as a single scalar `history` field on any primitive: `false` off, `true` on at the default capture depth, a number = capture depth (how many statement executions are recorded per history record \u2014 NOT record retention), `"all"` unlimited.
|
|
1821
|
+
'- **Request history** controls per-object execution capture (the request/task/trigger debugger). Authored as a single scalar `history` field on any primitive: `false` off, `true` on at the default capture depth, a number = capture depth (how many statement executions are recorded per history record \u2014 NOT record retention), `"all"` unlimited.',
|
|
1822
|
+
" - **Omit `history` to inherit** \u2014 the engine resolves object \u2192 container \u2192 workspace at request time (a query inherits from its API group, a tool from its toolset envelope, everything else straight from the workspace). Any authored value stops inheriting for that object.",
|
|
1823
|
+
" - Per-kind defaults (when inheriting): query/task/tool capture ON, function/trigger/middleware OFF; default depth 100.",
|
|
1824
|
+
" - Container tiers are authorable too \u2014 `apiGroup({ history })` sets the `query_*` default its queries inherit, and an agent/mcp_server/toolset `history` sets the `tool_*` default its tools inherit.",
|
|
1825
|
+
" - Workspace-level defaults are the terminal tier: `workspaceConfig({ history: { query, function, task, tool, trigger, middleware } })` emits the flat `{objType}_enabled`/`{objType}_limit` map (no inherit flag) \u2014 setting it is wholesale (unlisted types fall back to their engine default), so declare every default you want to keep; omit the field to leave existing workspace history untouched.",
|
|
1763
1826
|
"",
|
|
1764
1827
|
'- **Workspace environment variables** set a tenant\'s env vars through the workspace object: `workspaceConfig({ env: { STRIPE_KEY: process.env.STRIPE_KEY!, APP_BASE_URL: "https://\u2026" } })`. Author them as a name\u2192value MAP. Read a var back with `env("NAME")` (\u2192 `$env.NAME`), which compiles to tag "setting" with the plain name. Values are SECRETS: prefer sourcing from `process.env` over committing literals, and don\'t commit a compiled bundle with real values. `deploy` REPLACES the tenant\'s env with the declared map; `release` (merge) is ADD-ONLY \u2014 it creates missing keys but does NOT update or remove existing ones, so changing a value in code and releasing leaves the live value unchanged. Omit `env` to leave existing env untouched. The separate `settings` field is a plain object.',
|
|
1765
1828
|
""
|
|
@@ -1809,8 +1872,20 @@ function renderSections(m) {
|
|
|
1809
1872
|
"- `s.security.create_guid({ as? })` \u2014 bind a fresh GUID string. Takes nothing else.",
|
|
1810
1873
|
"- `s.function.run({ fn, input?, as?, runtime? })` / `s.function.call({ fn, input?, as? })` \u2014 run another function; `input` is keyed by the target's input names.",
|
|
1811
1874
|
' - `runtime?` runs it in the BACKGROUND: `{ mode: "async-shared" }` or `{ mode: "async-dedicated", cpu?, memory?, timeout?, maxRetry? }` (resources read at dedicated only). An async call DOES NOT return the result \u2014 it dispatches and continues, so `as` binds nothing; collect with `s.await({ ids })`. Omit for a normal call. Same block on `s.ai.agent.run`.',
|
|
1812
|
-
|
|
1813
|
-
|
|
1875
|
+
"- `s.api.call({ api, input?, headers?, auth?, as? })` \u2014 invoke an endpoint. WORKFLOW-TEST ONLY (see Gotchas in `llms.txt`).",
|
|
1876
|
+
" - `api` takes the `query()` def HANDLE (or a `{ name, guid }` pair) \u2014 a bare name is refused, because a query's identity is composed from its api group, verb, and name.",
|
|
1877
|
+
' - `headers` REPLACES the request headers the callee sees and takes the same shapes as `s.api.request`\'s \u2014 a `{ "Name": value }` record (values may be tagged), a `string[]` of `"Name: value"` lines, or one `Value`.',
|
|
1878
|
+
" - `auth` is `{ token, ignoreExpiration? }`: `token` must be a BARE STRING \u2014 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.",
|
|
1879
|
+
" - Neither slot authenticates the call today; see `llms/tests.md` for what a workflow-test run actually sees.",
|
|
1880
|
+
"- `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? })` \u2014 external HTTP request (`mvp:api_request`). Ergonomic types, each also accepting a dynamic `Value`:",
|
|
1881
|
+
" - `method` suggests the 7 verbs (GET/POST/PUT/DELETE/HEAD/OPTIONS/PATCH).",
|
|
1882
|
+
' - `params` a plain JSON object **or** a FLAT record whose values are tagged `Value`s (`{ count: ref("count") }`, each lifted via a `set` filter \u2014 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` (\u2192 query string for GET/HEAD/OPTIONS, body otherwise).',
|
|
1883
|
+
' - `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 \u2014 prefer a header over a `?key=` query param for a credential \u2014 a URL travels into access logs, proxies and `Referer`.',
|
|
1884
|
+
" - \u26A0 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 \u2014 read `response.result`.",
|
|
1885
|
+
" - 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.",
|
|
1886
|
+
" - `timeout` a `number` in seconds (1\u201386400), and `follow_location`/`verify_host`/`verify_peer` booleans.",
|
|
1887
|
+
" - `description` (Settings tab) and `output` filters (Output tab) ride the envelope. SSL cert interdependencies (certificate\u2194private_key, ca_certificate\u2192verify_peer) are checked at build time when statically provable, else by the engine at runtime.",
|
|
1888
|
+
" - 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`.",
|
|
1814
1889
|
"- `s.stream.from_request({ url, method?, \u2026tls, as? })` \u2014 streaming external HTTP request (`mvp:streaming_api_request`); same typed field surface as `s.api.request` (no description/output envelope). `url` is REQUIRED \u2014 it shares `api.request`'s engine declaration, which has no default.",
|
|
1815
1890
|
'- `s.util.send_email({ to, subject, message, from?, cc?, bcc?, reply_to?, service_provider?, api_key?, scheduled_at?, as? })` \u2014 send email from the stack. `service_provider` is `"xano"` (the built-in mailer \u2014 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.',
|
|
1816
1891
|
"- `s.webflow.request({ path, method?, \u2026tls, as? })` \u2014 Webflow API request (`mvp:connect_webflow_api_request`); like `s.api.request` but addressed by `path` (host is engine-supplied), and `path` is REQUIRED \u2014 the engine rejects an empty one. No `headers`: the engine builds its own from the workspace's Webflow connection and ignores an authored value.",
|
|
@@ -1821,9 +1896,18 @@ function renderSections(m) {
|
|
|
1821
1896
|
"",
|
|
1822
1897
|
"Microservices (the `microservice()` def and the statement that calls it):",
|
|
1823
1898
|
"",
|
|
1824
|
-
"- `microservice({ name, kind, \u2026 })` \u2014 two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` \u2014 passing both throws.
|
|
1899
|
+
"- `microservice({ name, kind, \u2026 })` \u2014 two mutually exclusive shapes via `kind`: `builtin` declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and `helm` points at a chart and its `values` \u2014 passing both throws.",
|
|
1900
|
+
" - EARLY SURFACE, expected to change \u2014 every export of a workspace declaring one prints a notice saying so.",
|
|
1901
|
+
" - `configs`/`volumes` are typed and `@deprecated` but NOT deployable: the engine rejects an import carrying either, so `export()` fails the build rather than letting the deploy fatal.",
|
|
1902
|
+
" - Put a value the workload reads in a container's `env`, and storage in a container's own `volumes` (`emptyDir`/`persistent`/`config`).",
|
|
1903
|
+
" - Container names are free-form \u2014 they need not match the microservice name, which is what a stack addresses.",
|
|
1825
1904
|
'- SECRETS RIDE ALONG \u2014 `chart.values` and `registryAuth.dockerconfigjson` are carried into the bundle, and into a pulled tree, verbatim (they must be, or a pulled microservice could not be redeployed). Both are stored strings with NO deploy-time indirection: `process.env.X` in the def resolves at EXPORT and writes the literal into the bundle, so it is not a way to keep the credential out. Either leave `registryAuth` unset (public image, or a credential attached outside this workspace) or treat the bundle and any pulled tree as secret material \u2014 keep them out of git, or rotate after. Export prints a notice per microservice carrying either field; `--strict` does not promote it. For a secret a STACK reads, the mapped surface is `workspaceConfig({ env })` + `env("NAME")`.',
|
|
1826
|
-
|
|
1905
|
+
"- `s.microservice.request({ host, path, port?, method?, params?, headers?, timeout?, follow_location?, as? })` \u2014 in-cluster microservice call (`mvp:microservice_request`); no TLS fields.",
|
|
1906
|
+
" - ONLY `host`+`path` required; the rest default to the engine's values (`GET`/`{}`/`[]`/`10`/`true`), always emitted.",
|
|
1907
|
+
" - Pass the `microservice()` DEF as `host` \u2014 it binds by NAME (how the engine resolves it), so a rename fixes every call site and the port is checked before deploy.",
|
|
1908
|
+
' - `port?` folds into `host` as `"name:port"`: a def exposing ONE `servicePort` resolves automatically, SEVERAL requires it.',
|
|
1909
|
+
' - A raw `"name:port"` string works, unvalidated, and is the only way to reach an instance-level microservice.',
|
|
1910
|
+
' - `tenantDeploy: "manual"` on the def imports the row without starting the workload.',
|
|
1827
1911
|
"- `s.workflow_test.call({ workflowTest, datasource?, as? })` \u2014 run another workflow test from inside one, which is the only place it runs (see Gotchas in `llms.txt`). The odd one out: NO `input` (a workflow test takes none), and it carries `datasource?` instead \u2014 same clone caveat as the kind's own field.",
|
|
1828
1912
|
""
|
|
1829
1913
|
];
|
|
@@ -1921,6 +2005,8 @@ function renderSections(m) {
|
|
|
1921
2005
|
legacy,
|
|
1922
2006
|
objectKinds,
|
|
1923
2007
|
kindsCore,
|
|
2008
|
+
client,
|
|
2009
|
+
errors,
|
|
1924
2010
|
kindsAgentMcp,
|
|
1925
2011
|
kindsKnowledge,
|
|
1926
2012
|
kindsRealtime,
|
|
@@ -1953,7 +2039,7 @@ function renderDocs(m) {
|
|
|
1953
2039
|
{
|
|
1954
2040
|
path: "llms/object-kinds.md",
|
|
1955
2041
|
title: "Object kinds",
|
|
1956
|
-
readWhen: "Read when deciding WHAT to build \u2014 every authorable primitive
|
|
2042
|
+
readWhen: "Read when deciding WHAT to build \u2014 every authorable primitive, what each is, and which factory + register method builds it. Also when `register*` will not typecheck an array built by `.flatMap()`/`.concat()`.",
|
|
1957
2043
|
body: s.objectKinds,
|
|
1958
2044
|
dropLeadingHeading: true
|
|
1959
2045
|
},
|
|
@@ -1964,10 +2050,22 @@ function renderDocs(m) {
|
|
|
1964
2050
|
body: s.kindsCore,
|
|
1965
2051
|
dropLeadingHeading: true
|
|
1966
2052
|
},
|
|
2053
|
+
{
|
|
2054
|
+
path: "llms/client.md",
|
|
2055
|
+
title: "Consuming defs from a client",
|
|
2056
|
+
readWhen: "Read when a frontend imports a def (`getPath()`/`InferResponse`) or calls the deployed backend \u2014 what the import costs, `window.XANO_HOST`, local dev, file URLs, and a Node spot-check.",
|
|
2057
|
+
body: s.client
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
path: "llms/errors.md",
|
|
2061
|
+
title: "Error index",
|
|
2062
|
+
readWhen: "Read when a request, test, build, or pull fails with a message you did not write \u2014 the string, its cause, and where the fix is.",
|
|
2063
|
+
body: s.errors
|
|
2064
|
+
},
|
|
1967
2065
|
{
|
|
1968
2066
|
path: "llms/tests.md",
|
|
1969
2067
|
title: "Saved unit tests, assertions, and mocks",
|
|
1970
|
-
readWhen: "Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement
|
|
2068
|
+
readWhen: "Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement needs a `mock`, or when running a deployed environment's tests.",
|
|
1971
2069
|
body: s.tests,
|
|
1972
2070
|
dropLeadingHeading: true
|
|
1973
2071
|
},
|
|
@@ -1980,7 +2078,7 @@ function renderDocs(m) {
|
|
|
1980
2078
|
{
|
|
1981
2079
|
path: "llms/kinds-knowledge.md",
|
|
1982
2080
|
title: "Knowledge def shape",
|
|
1983
|
-
readWhen: "Read when the workspace defines a `knowledge()` item
|
|
2081
|
+
readWhen: "Read when the workspace defines a `knowledge()` item for its AI agents.",
|
|
1984
2082
|
body: s.kindsKnowledge
|
|
1985
2083
|
},
|
|
1986
2084
|
{
|
|
@@ -2005,7 +2103,7 @@ function renderDocs(m) {
|
|
|
2005
2103
|
{
|
|
2006
2104
|
path: "llms/statements-runtime.md",
|
|
2007
2105
|
title: "Statement runtime behavior",
|
|
2008
|
-
readWhen: "Read when you need to know what a statement's `as:` output
|
|
2106
|
+
readWhen: "Read when you need to know what a statement's `as:` output holds, or why a bound variable is not the shape expected.",
|
|
2009
2107
|
body: s.stmtRuntime
|
|
2010
2108
|
},
|
|
2011
2109
|
{
|
|
@@ -2024,7 +2122,7 @@ function renderDocs(m) {
|
|
|
2024
2122
|
{
|
|
2025
2123
|
path: "llms/fields.md",
|
|
2026
2124
|
title: "Column and input types",
|
|
2027
|
-
readWhen: "Read when declaring a table column (`f.*`) or a function/query input (`input.*`) \u2014 a type's options and
|
|
2125
|
+
readWhen: "Read when declaring a table column (`f.*`) or a function/query input (`input.*`) \u2014 a type's options and methods, and the `s.precondition` error/status contract. Also `table({ seed })` rows and `use_xdo`.",
|
|
2028
2126
|
body: s.fields,
|
|
2029
2127
|
dropLeadingHeading: true
|
|
2030
2128
|
},
|
|
@@ -2038,7 +2136,7 @@ function renderDocs(m) {
|
|
|
2038
2136
|
{
|
|
2039
2137
|
path: "llms/lambda.md",
|
|
2040
2138
|
title: "Lambda bodies (JavaScript)",
|
|
2041
|
-
readWhen: "Read when writing a JavaScript body, or weighing whether to reach for one at all \u2014 `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda
|
|
2139
|
+
readWhen: "Read when writing a JavaScript body, or weighing whether to reach for one at all \u2014 `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`; each surface binds a different set of identifiers.",
|
|
2042
2140
|
body: s.lambda,
|
|
2043
2141
|
dropLeadingHeading: true
|
|
2044
2142
|
},
|
|
@@ -2052,14 +2150,14 @@ function renderDocs(m) {
|
|
|
2052
2150
|
{
|
|
2053
2151
|
path: "llms/legacy.md",
|
|
2054
2152
|
title: "Legacy paradigms and retired statements",
|
|
2055
|
-
readWhen: 'Read when the code was PULLED from an existing Xano instance
|
|
2153
|
+
readWhen: 'Read when the code was PULLED from an existing Xano instance \u2014 how a codegen\'d tree reads, the shapes in it that must not be "fixed", and any `raw({ name: "mvp:\u2026" })`, `realtimeTrigger()`, or name the catalogs do not list.',
|
|
2056
2154
|
body: s.legacy,
|
|
2057
2155
|
dropLeadingHeading: true
|
|
2058
2156
|
},
|
|
2059
2157
|
{
|
|
2060
2158
|
path: "llms/statements-catalog.md",
|
|
2061
2159
|
title: "Statement catalog",
|
|
2062
|
-
readWhen: "Read for the field signature of a specific statement \u2014 every surface, grouped by `s.*` namespace.
|
|
2160
|
+
readWhen: "Read for the field signature of a specific statement \u2014 every surface, grouped by `s.*` namespace.",
|
|
2063
2161
|
body: s.stmtCatalog
|
|
2064
2162
|
}
|
|
2065
2163
|
];
|