@xanots/sdk 0.0.13 → 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 +18 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/{agent-file-refresh-GQWAAOBV.js → agent-file-refresh-2LZ2YRFZ.js} +3 -3
- package/dist/bin.js +2 -2
- package/dist/{chunk-2AY3PKF4.js → chunk-CII2TOE4.js} +2 -2
- package/dist/{chunk-2VTJSI6X.js → chunk-F3MRG7NI.js} +2 -2
- package/dist/{chunk-RLI6XD4O.js → chunk-LISUHLWI.js} +9 -9
- package/dist/{chunk-S3DOJOW4.js → chunk-ORZINVGQ.js} +4 -4
- package/dist/{chunk-SS2V2QOG.js → chunk-UQMMFW32.js} +4 -4
- package/dist/{chunk-OONT4ZL4.js → chunk-UXPDQFG2.js} +2 -2
- package/dist/{chunk-VK26K7AY.js → chunk-XBIFQWZM.js} +2 -2
- package/dist/cli.js +1 -1
- package/dist/{codegen-command-ZDHMGFWZ.js → codegen-command-5A6FVKUA.js} +5 -5
- package/dist/{deploy-command-CGVKRWUE.js → deploy-command-VBB7JYXH.js} +3 -3
- package/dist/{init-command-DNDONP3O.js → init-command-SIJUH2DD.js} +4 -4
- package/dist/internal.js +357 -259
- package/dist/{lock-commands-6U7UIJGR.js → lock-commands-4TX3BONI.js} +2 -2
- package/dist/node.js +1 -1
- package/dist/{preflight-command-UYE7SUQV.js → preflight-command-MTYGPNVT.js} +3 -3
- package/dist/{release-command-CMMYT6XK.js → release-command-NXCPQDFF.js} +5 -5
- package/dist/{upgrade-command-A75DOIUH.js → upgrade-command-GSH3M3N6.js} +5 -5
- package/dist/{workspace-command-YELP47SJ.js → workspace-command-XUOPLVQ4.js} +5 -5
- package/guides/README.md +1 -1
- package/guides/coverage.md +3 -2
- 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 +1 -1
- 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 +2 -2
- package/llms/triggers.md +14 -2
- package/llms/values.md +2 -2
- package/llms-full.txt +359 -267
- package/llms.txt +163 -234
- package/manifest.json +4 -4
- package/package.json +1 -1
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.14
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -15,11 +15,11 @@ TAGGED value — `ref("x")` a stack variable, `inp("x")` an input, `auth("id")`
|
|
|
15
15
|
caller, `c.*` a constant — resolved at request time; JS operators over them do not
|
|
16
16
|
compute (see Gotchas). Requests share no memory — state persists in tables or redis.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Authorable: statement surfaces 214/214, filters 226 (226 typed).
|
|
19
19
|
Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull; reasons in `coverage.objectKinds.unmodeled`.
|
|
20
20
|
|
|
21
21
|
This file is the whole always-read surface: the mental model, the deploy contract,
|
|
22
|
-
every gotcha, and control flow. Per-surface detail lives in the topic files listed
|
|
22
|
+
every cross-cutting gotcha, and control flow. Per-surface detail lives in the topic files listed
|
|
23
23
|
below — open the one whose condition matches the task, skip the rest. For
|
|
24
24
|
exhaustive per-entry detail in NEITHER — a statement's field schema with engine
|
|
25
25
|
defaults, a filter's full argument list, the `storedName` mapping — do a TARGETED
|
|
@@ -37,23 +37,25 @@ Paths are relative to this file (`node_modules/@xanots/sdk/` once installed), so
|
|
|
37
37
|
plain file read resolves them at the version you have.
|
|
38
38
|
`llms-full.txt` is everything concatenated — one fetch for a reader that cannot open files.
|
|
39
39
|
|
|
40
|
-
- [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — every authorable primitive
|
|
40
|
+
- [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — 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()`.
|
|
41
41
|
- [Core def shapes](llms/kinds-core.md): Read when authoring a function, query, api group, task, workflow test, middleware, or tool — and for the `response` and `expr` shapes every one of them uses.
|
|
42
|
-
- [
|
|
42
|
+
- [Consuming defs from a client](llms/client.md): Read when a frontend imports a def (`getPath()`/`InferResponse`) or calls the deployed backend — what the import costs, `window.XANO_HOST`, local dev, file URLs, and a Node spot-check.
|
|
43
|
+
- [Error index](llms/errors.md): Read when a request, test, build, or pull fails with a message you did not write — the string, its cause, and where the fix is.
|
|
44
|
+
- [Saved unit tests, assertions, and mocks](llms/tests.md): Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement needs a `mock`, or when running a deployed environment's tests.
|
|
43
45
|
- [Agent and MCP def shapes](llms/kinds-agent-mcp.md): Read when the workspace defines an `agent()` or an `mcpServer()`.
|
|
44
|
-
- [Knowledge def shape](llms/kinds-knowledge.md): Read when the workspace defines a `knowledge()` item
|
|
46
|
+
- [Knowledge def shape](llms/kinds-knowledge.md): Read when the workspace defines a `knowledge()` item for its AI agents.
|
|
45
47
|
- [Realtime def shapes](llms/kinds-realtime.md): Read when the workspace defines a `realtimeServer()`, `realtimeChannel()`, or `realtimeMessage()` handler.
|
|
46
48
|
- [Triggers](llms/triggers.md): Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.
|
|
47
49
|
- [Array and database statements](llms/statements-data.md): Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).
|
|
48
|
-
- [Statement runtime behavior](llms/statements-runtime.md): Read when you need to know what a statement's `as:` output
|
|
50
|
+
- [Statement runtime behavior](llms/statements-runtime.md): Read when you need to know what a statement's `as:` output holds, or why a bound variable is not the shape expected.
|
|
49
51
|
- [Auth, cross-object calls, and microservices](llms/statements-calls.md): Read when the stack authenticates (the signup/login/token recipe is here), calls another object (`s.function.run`, `s.api.request`, `s.tool.call`), sends email (`s.util.send_email`), or reaches a microservice.
|
|
50
52
|
- [Value catalog](llms/values.md): Read when you need a literal, a reference, or a tag you have not used before — `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.
|
|
51
|
-
- [Column and input types](llms/fields.md): Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and
|
|
53
|
+
- [Column and input types](llms/fields.md): Read when declaring a table column (`f.*`) or a function/query input (`input.*`) — a type's options and methods, and the `s.precondition` error/status contract. Also `table({ seed })` rows and `use_xdo`.
|
|
52
54
|
- [Filter catalog](llms/filters.md): Read when piping a value through `fl.*` — the full catalog with each filter's argument types.
|
|
53
|
-
- [Lambda bodies (JavaScript)](llms/lambda.md): Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda
|
|
55
|
+
- [Lambda bodies (JavaScript)](llms/lambda.md): Read when writing a JavaScript body, or weighing whether to reach for one at all — `lam.fn`, `fl.lambda`, `fl.reduce`, `s.lambda`; each surface binds a different set of identifiers.
|
|
54
56
|
- [Lock file](llms/lock.md): Read when a `xano.lock` exists or should — renaming/pruning/importing identities, seeding the lock programmatically, or asking which commands write it.
|
|
55
|
-
- [Legacy paradigms and retired statements](llms/legacy.md): Read when the code was PULLED from an existing Xano instance
|
|
56
|
-
- [Statement catalog](llms/statements-catalog.md): Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace.
|
|
57
|
+
- [Legacy paradigms and retired statements](llms/legacy.md): Read when the code was PULLED from an existing Xano instance — how a codegen'd tree reads, the shapes in it that must not be "fixed", and any `raw({ name: "mvp:…" })`, `realtimeTrigger()`, or name the catalogs do not list.
|
|
58
|
+
- [Statement catalog](llms/statements-catalog.md): Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace.
|
|
57
59
|
|
|
58
60
|
## Quickstart
|
|
59
61
|
|
|
@@ -125,6 +127,8 @@ operand, a `ref()` nothing binds). Each has a legitimate use, so each only warns
|
|
|
125
127
|
but nothing fails on a message no one reads, so in CI and in unattended agent builds
|
|
126
128
|
pass `--strict` (`emitBundle(app, { strict: true })` / `app.export({ strict: true })`):
|
|
127
129
|
every warning becomes a hard failure. Same bundle bytes either way.
|
|
130
|
+
Before calling the work done: `npm run typecheck`, `xanots export <entry> --strict`,
|
|
131
|
+
`xanots deploy <entry>`, then `xanots test run-all` against what deployed.
|
|
128
132
|
|
|
129
133
|
Identity: object guids derive from `(type, name)` — a query's from `(api group,
|
|
130
134
|
verb, name)` — so renames change identity.
|
|
@@ -170,90 +174,89 @@ production workspace, but confirm with the user before the first run.
|
|
|
170
174
|
|
|
171
175
|
**Frontend wiring.** `--static <dir>` injects the DEPLOYED env's backend URL as
|
|
172
176
|
`window.XANO_HOST` into EVERY html document in the build, before the app bundle runs,
|
|
173
|
-
so the frontend needs no rebuild to target an env.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
fallback:
|
|
177
|
-
const HOST = (typeof window !== 'undefined' && window.XANO_HOST) || import.meta.env.VITE_XANO_HOST;
|
|
178
|
-
In LOCAL DEV there is no injected global, so the fallback is what answers: set
|
|
179
|
-
`VITE_XANO_HOST` in a `.env.local` beside `.env.example` at the PROJECT ROOT. The
|
|
180
|
-
scaffold's vite config sets `envDir` there (its `root` is `frontend/`, and Vite
|
|
181
|
-
resolves `.env` files against `root`) — without it the var reads as undefined, the
|
|
182
|
-
host falls back to '', and every call 404s off the dev server.
|
|
183
|
-
⚠ It is INJECTED in bracket form — `window["XANO_HOST"]="…"` — so verifying a deploy
|
|
184
|
-
by grepping `window.XANO_HOST` matches nothing and reads as a failed inject. Grep the
|
|
185
|
-
bare `XANO_HOST` token.
|
|
186
|
-
⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
187
|
-
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
188
|
-
**Displaying a stored file.** A file column comes back as `{ path, name, type, size,
|
|
189
|
-
meta, access, url }`. ⚠ Do NOT use its `url`: on a tenant-scoped environment that field
|
|
190
|
-
addresses the instance host WITHOUT the `/tenant/<name>` segment and 404s, silently —
|
|
191
|
-
as a broken `<img>`, with every API assertion still passing. Build the URL from `path`
|
|
192
|
-
instead: `fileUrl(row.avatar, HOST)` (exported from `@xanots/sdk`) returns `null` for
|
|
193
|
-
an absent file and is correct on an ephemeral and an instance workspace alike.
|
|
194
|
-
⚠ A static host serves these files verbatim, so everything injected is PUBLIC — base
|
|
177
|
+
so the frontend needs no rebuild to target an env. How to read it, local dev, verifying
|
|
178
|
+
the inject, and displaying a stored file: `llms/client.md`.
|
|
179
|
+
⚠ A static host serves its files verbatim, so everything injected is PUBLIC — base
|
|
195
180
|
URLs and publishable keys only, never secrets. Secrets go in backend env, read
|
|
196
181
|
server-side via `env(name)`.
|
|
182
|
+
⚠ `xanots preflight` ignores the deploy login and reads `XANO_VALIDATE_INSTANCE` /
|
|
183
|
+
`XANO_VALIDATE_TOKEN` (+ optional `XANO_VALIDATE_WORKSPACE_ID`) from the environment.
|
|
197
184
|
|
|
198
185
|
**Full CLI surface:** `xanots <command> --help` lists every command, flag, and
|
|
199
186
|
default; the shipped `manifest.json` carries the same in its `cli` array. This doc
|
|
200
187
|
does not duplicate it — it covers what you must know to AUTHOR a workspace.
|
|
201
|
-
**
|
|
202
|
-
(`
|
|
203
|
-
flat factory aliases (`dbAdd`, `dbQuery`, `setVar`, `mathAdd`, …) are exported
|
|
204
|
-
and identical in output; prefer `s.*` in new code so examples stay consistent.
|
|
188
|
+
**Style:** reach statements through the `s` namespace (`s.db.add`, `s.math.add`, …).
|
|
189
|
+
The flat aliases (`dbAdd`, `setVar`, …) are identical in output; prefer `s.*`.
|
|
205
190
|
|
|
206
191
|
## Gotchas
|
|
207
192
|
|
|
208
193
|
Non-obvious authoring rules:
|
|
209
194
|
|
|
195
|
+
### Values and references
|
|
196
|
+
|
|
210
197
|
- **No callback builder.** Flat def-objects + `register*`, not
|
|
211
198
|
`workspace(w => w.table(...))`. `workspace(name)` returns a named `new Xano()`;
|
|
212
199
|
tables are `table({ schema: { col: f.text() } })`.
|
|
213
|
-
- **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a
|
|
214
|
-
stack variable (a value); `f.tableRef` is the column constructor.
|
|
215
200
|
- **Reference-helper picker:** `ref` = stack var (`as:` output), `inp` = input,
|
|
216
201
|
`col` = table column (in `db.query` `where`), `auth("id")` = the caller,
|
|
217
202
|
`c.*` = a constant. Pick by what you're pointing at.
|
|
203
|
+
- **Foreign key is `f.tableRef(table)`, not `ref`.** `ref(name)` references a
|
|
204
|
+
stack variable (a value); `f.tableRef` is the column constructor.
|
|
218
205
|
- **Tagged values are DATA — a JS template literal cannot compose them.** `${ref(...)}`
|
|
219
206
|
(or `"a" + ref(...)`) stringifies the tag object at BUILD time: ``c.text(`Hi ${ref("u.name")}`)``
|
|
220
207
|
type-checks and encodes the literal text `Hi [object Object]`, served verbatim.
|
|
221
208
|
`export()` warns; `--strict` fails. Compose at RUNTIME:
|
|
222
209
|
`withFilters(c.text("Hi "), fl.concat(ref("u.name")))`, an `obj({...})`/record member,
|
|
223
210
|
or `c.expression('"Hi, " ~ $var.u.name')`.
|
|
224
|
-
-
|
|
225
|
-
|
|
226
|
-
the
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
A
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
`
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
211
|
+
- **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`
|
|
212
|
+
resolves ONLY against this def's own `input` block — a value produced earlier in
|
|
213
|
+
the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at
|
|
214
|
+
runtime with `Unable to locate input` (`export()` warns).
|
|
215
|
+
- **`get_input`/`get_raw_input` read the whole payload**, not one named input
|
|
216
|
+
(args are `{ as?, encoding?, excludeMiddleware? }` — no `name`). For a single
|
|
217
|
+
input use `inp("name")`.
|
|
218
|
+
- **To match every row, omit `where` — never pass a constant.** `where: c.bool(true)` is
|
|
219
|
+
not "no filter": a bare scalar literal states no condition and the engine reads the
|
|
220
|
+
resulting `context.search` as garbage, so it throws. An absent `where` is how "every
|
|
221
|
+
row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built
|
|
222
|
+
elsewhere — `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.
|
|
223
|
+
|
|
224
|
+
### Tables and columns
|
|
225
|
+
|
|
226
|
+
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
227
|
+
every table (`system: true` by default); declaring them by hand is redundant.
|
|
228
|
+
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
229
|
+
Both are valid targets wherever a column name is accepted — `db.query` `sort`/
|
|
230
|
+
`output`, a `db.get`/`edit`/`del` `fieldName`, etc. (the column-name type is
|
|
231
|
+
`keyof schema | "id" | "created_at"`), and both appear in `InferRow<typeof table>`.
|
|
232
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
233
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
234
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
235
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
236
|
+
- **Self-referencing tables** need the bare-name form: inside `tweets`'s own
|
|
237
|
+
schema, write `f.tableRef("tweets", { type: "int" })` — the `const tweets`
|
|
238
|
+
handle isn't assigned yet, so the handle form throws "used before declaration".
|
|
239
|
+
- **Seed a table's starting rows with `table({ seed })`.** Rows typed against the
|
|
240
|
+
schema, inline (`seed: [{ name: "…" }]`) or from a file (`seedFile("./seed.json",
|
|
241
|
+
import.meta.url)`). Only `deploy` ships them, and a re-deploy re-seeds cleanly. Never
|
|
242
|
+
put secrets in `seed`. Row shape, `id` pinning, and the file rules: `llms/fields.md`.
|
|
243
|
+
- **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**
|
|
244
|
+
A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime
|
|
245
|
+
with `Unable to locate var: u.password` — the column is simply absent from the row.
|
|
246
|
+
Name it in the read's `output` to pull it: `s.db.get({ table: users, fieldName: "email",
|
|
247
|
+
fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then
|
|
248
|
+
`s.security.check_password`. `output` OVERRIDES column visibility — it is the only way to
|
|
249
|
+
read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not
|
|
250
|
+
return.
|
|
251
|
+
- **Don't take a password through `input.password` on login — it double-hashes.**
|
|
252
|
+
An `f.password()` column hashes on write, and `input.password` *also* hashes the
|
|
253
|
+
submission on bind, so `s.security.check_password` compares two different hashes
|
|
254
|
+
and a correct password always fails (`ok:false` on a found row). Take the submitted
|
|
255
|
+
password as `input.text()` on both signup and login and pass the plaintext straight
|
|
256
|
+
to `check_password` (which does the comparison hash itself). `export()` warns.
|
|
257
|
+
|
|
258
|
+
### Reading and writing rows
|
|
259
|
+
|
|
257
260
|
- **DB reads are field-match, not `where`-expr.** `db.get`/`db.edit`/`db.del`/
|
|
258
261
|
`db.has`/`db.patch` match one field: `{ fieldName, fieldValue }` (`fieldName`
|
|
259
262
|
defaults to the PK `id`). Only `db.query` takes a `where`/`additionalWhere`
|
|
@@ -267,44 +270,73 @@ Non-obvious authoring rules:
|
|
|
267
270
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
268
271
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
269
272
|
check to the client.
|
|
270
|
-
- **
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
`
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
273
|
+
- **Drilling into a maybe-null `db.get` result 500s — use `ref(path, { safe: true })`.**
|
|
274
|
+
`db.get` binds `null` on a no-match, but a nested `ref("owner.user_id")` raises a runtime
|
|
275
|
+
"Unable to locate var" (HTTP 500) when `owner` is null — an ownership guard throws instead
|
|
276
|
+
of failing cleanly. Two correct shapes:
|
|
277
|
+
- Guard existence first — `s.precondition({ expr: expr(ref("owner"), "!=", c.null()),
|
|
278
|
+
error_type: "notfound", error: c.text("Not found.") })` — then drill WITHOUT `safe`.
|
|
279
|
+
- Or drill null-safe inside an EXPRESSION or `obj()` operand:
|
|
280
|
+
`expr(ref("owner.user_id", { safe: true }), "=", auth("id"))` yields `null` (the guard
|
|
281
|
+
reads `false`) instead of 500ing. Works inside `obj({...})` too — no `s.set_var` hoist needed.
|
|
282
|
+
⚠ EXPRESSION and `obj()` operands ONLY — never a `db.*` match argument. `null` is not a
|
|
283
|
+
legal `fieldValue`/`id`, so a chained get (fetch the child row, then its parent to check
|
|
284
|
+
the owner) fails with HTTP 400 `Missing param: field_value` one statement BEFORE the guard.
|
|
285
|
+
There guarding existence first is MANDATORY. `export()` warns on a safe ref in that position.
|
|
286
|
+
⚠ That is for a base that EXISTS and may be null. If the base binds nothing at all (a typo),
|
|
287
|
+
`{ safe: true }` buries the bug as a silent `null` — `export()` warns with a did-you-mean;
|
|
288
|
+
fix the name instead.
|
|
289
|
+
|
|
290
|
+
### Stacks and calls
|
|
291
|
+
|
|
292
|
+
- **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/
|
|
293
|
+
`s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
|
|
294
|
+
take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
|
|
295
|
+
`s.group(body)` and `s.util.post_process(body)` take it **positionally**.
|
|
296
|
+
`s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
|
|
297
|
+
authored signatures in `llms/statements-data.md`.
|
|
298
|
+
- **`s.api.call` / `s.task.call` / `s.trigger.call` / `s.workflow_test.call` are
|
|
299
|
+
WORKFLOW-TEST ONLY.** Outside a `workflowTest({...})` stack the engine cannot reach
|
|
300
|
+
the target, so one in a query/function/task deploys clean and then answers the first
|
|
301
|
+
real request with `ERROR_FATAL: <Type> does not exist: <type>:<n>` — and not per
|
|
302
|
+
host kind, the same call fails identically from a function a query runs.
|
|
303
|
+
`export()` refuses them. `s.function.call`, `s.tool.call`, `s.middleware.call` and
|
|
304
|
+
`s.addon.call` run anywhere, as does `s.function.run` (the ordinary function
|
|
305
|
+
invocation). To share logic between endpoints, put it in a `defineFunction` and
|
|
306
|
+
`s.function.run` it from both.
|
|
307
|
+
- **`expect.*` is a unit-test assertion; `s.expect.*` is a workflow-test statement.**
|
|
308
|
+
A statement's `mock` is keyed by TEST NAME; an undeclared name throws. See `llms/tests.md`.
|
|
309
|
+
- **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**
|
|
310
|
+
Spreading `...requireX()` where the helper is typed `Statement[]` drops the stack's TUPLE
|
|
311
|
+
type, so EVERY `ref()`/`as` in that stack — even ones after the spread — resolves to
|
|
312
|
+
`unknown` and the response types as `StackTupleWidened`. Nothing fails at the helper; it
|
|
313
|
+
surfaces where the response is consumed, often a frontend typecheck. Fix: return
|
|
314
|
+
`statements(s.a(...), s.b(...))` — its tuple survives the spread. A helper that builds
|
|
315
|
+
its array in a LOOP cannot be a tuple; declare `responseShape` on the query there.
|
|
316
|
+
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
317
|
+
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are PHP `preg_*`: the
|
|
318
|
+
piped value is the PATTERN and must be delimiter-wrapped, and the ARGUMENT is the subject.
|
|
319
|
+
Correct: `withFilters(c.regex(/^[a-z-]+$/i), fl.regex_test(inp("slug")))`.
|
|
320
|
+
- A bare `c.text("^…$")` is an invalid pattern that matches *nothing* for every input,
|
|
321
|
+
so a precondition on it silently rejects all values. `c.regex(body, "i")` wraps +
|
|
322
|
+
escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
|
|
323
|
+
- Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
|
|
324
|
+
answers false for EVERY input, so an `if (matches) reject` guard admits what it refuses.
|
|
325
|
+
- Build time refuses both: `withFilters` throws on a bare `c.text` pattern from ANY position
|
|
326
|
+
in the chain (a normalizer in front of the regex filter is refused too; nothing upstream
|
|
327
|
+
adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match` is the
|
|
328
|
+
same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
|
|
329
|
+
`export()` warns on a reversed pair in stored bytes.
|
|
330
|
+
- Better still: a native typed input (`input.email`) over hand-rolled validation.
|
|
331
|
+
- **An `auth()`-keyed limiter collapses on any host with no caller identity — at every**
|
|
332
|
+
**tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))) })`
|
|
333
|
+
is the per-user form, but `auth()` is `null` on a public query, a task, or a function,
|
|
334
|
+
and every caller then shares one bucket. Attaching it once at
|
|
335
|
+
`apiGroup({ middleware })` or the workspace tier is the tempting DRY move and inherits
|
|
336
|
+
the same collapse onto every member endpoint. Export warns, naming the tier.
|
|
337
|
+
|
|
338
|
+
### Identity, build, and deploy
|
|
339
|
+
|
|
308
340
|
- **Same-name siblings collide — queries excepted: their identity carries group + verb.**
|
|
309
341
|
Guids derive from `(type, name)`, so two functions (or tables, toolsets, …) sharing
|
|
310
342
|
a name derive ONE guid and `export()` throws — give them DISTINCT names. A QUERY
|
|
@@ -315,137 +347,34 @@ Non-obvious authoring rules:
|
|
|
315
347
|
is NOT a lasting fix: a lock entry cannot hold two guids, so `export --lock`
|
|
316
348
|
refuses the pair (`export()` warns even unlocked); it is for pinning identity
|
|
317
349
|
across a rename, not for sharing a name.
|
|
318
|
-
- **`export()` vs `emitBundle()` vs `writeBundle()`:** `
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
331
|
-
the lock model) is on `@xanots/sdk/internal` and is never needed to author.
|
|
332
|
-
READING a bundle back is `@xanots/sdk/bundle` — a statement walker (`2.if.0` paths),
|
|
333
|
-
a structural hash, `mvp:*` catalog, `tableRefOf`.
|
|
334
|
-
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
335
|
-
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
336
|
-
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
337
|
-
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
338
|
-
⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
|
|
339
|
-
never removes it. The floor is the RUNTIME, not the def: a second or much richer def
|
|
340
|
-
adds ~2 kB, so trimming a def does not shrink it.
|
|
341
|
-
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
342
|
-
plain data importing NOTHING, still compile-checked: `routePath("GET blog/{slug}", { slug })`
|
|
343
|
-
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
344
|
-
URLs lifted to `wss://h/ws/<tenant>:<canonical>`). A rename is a type error, not a 404.
|
|
350
|
+
- **`export()` vs `emitBundle()` vs `writeBundle()`:** `export()` returns the bundle object,
|
|
351
|
+
`emitBundle()` the pretty JSON string, `writeBundle(app, path)` writes it to disk. All
|
|
352
|
+
three run the SAME build-time checks, INCLUDING seed validation of a literal `seed: [...]`
|
|
353
|
+
array (row shape, unknown column, coercion, enum membership, the all-or-nothing `id`
|
|
354
|
+
rule). A DEFERRED seed — a thunk (`() => import("./seed.json")`) or `seedFile()` — is
|
|
355
|
+
checked only by the `xanots export`/`deploy` CLI path. Seed rows are never emitted into
|
|
356
|
+
the bundle either way; only `deploy` ships them.
|
|
357
|
+
- Entry points: the `node:fs` writers (`writeBundle`/`writeArtifact`) and lock-file I/O
|
|
358
|
+
import from `@xanots/sdk/node`, NOT the browser-safe `@xanots/sdk` entry (a frontend
|
|
359
|
+
imports defs from it for `getPath()`/`InferInput`). `@xanots/sdk/internal` is the
|
|
360
|
+
compiler machinery, never needed to author. READING a bundle back is `@xanots/sdk/bundle`
|
|
361
|
+
— a statement walker (`2.if.0` paths), a structural hash, `mvp:*` catalog, `tableRefOf`.
|
|
345
362
|
- **Intra-workspace imports use `.js` specifiers** (`../tables/links.js`), not
|
|
346
363
|
extensionless — the defs compile under `moduleResolution: bundler`. Add the `.js`.
|
|
347
|
-
- **Verifying a def outside a bundler.** Inside a bundler (Vite/webpack) importing a
|
|
348
|
-
query def to read `getPath()`/`verb` works directly. To spot-check from Node, run a REAL
|
|
349
|
-
file with `tsx <file.ts>` **from inside the project root** — not `tsx -e "import …"`
|
|
350
|
-
(its CJS-preparse mis-resolves the package `exports` map → ERR_PACKAGE_PATH_NOT_EXPORTED),
|
|
351
|
-
and not bare `node file.ts` (chokes on the `.js`-specifier intra-workspace imports the
|
|
352
|
-
xanots CLI's own loader resolves). Running from outside the project root also breaks
|
|
353
|
-
the `@xanots/sdk` specifier resolution.
|
|
354
|
-
- **A helper returning `Statement[]` widens the stack and kills `InferResponse`.**
|
|
355
|
-
Factoring a repeated guard into `function requireX(): Statement[]` and spreading it
|
|
356
|
-
(`...requireX("d")`) drops the stack's TUPLE type, so EVERY `ref()`/`as` in that stack —
|
|
357
|
-
including ones declared after the spread — resolves to `unknown` and the query's
|
|
358
|
-
response types as `StackTupleWidened`. Nothing fails at the helper: the error surfaces
|
|
359
|
-
wherever the response is consumed, often a frontend package's typecheck. Fix: return
|
|
360
|
-
`statements(s.a(...), s.b(...))` — a const-generic identity export whose tuple survives
|
|
361
|
-
the spread. A helper that builds its array in a LOOP cannot be a tuple; declare
|
|
362
|
-
`responseShape` on the query there. See `llms/statements-runtime.md`.
|
|
363
|
-
- **Block specials nest a `body`, not a `stack`.** `s.for`/`s.foreach`/
|
|
364
|
-
`s.while`/`s.switch`/`s.try_catch`/`s.db.transaction`/`s.expect.to_throw`
|
|
365
|
-
take their sub-stack as `body` (`try`/`catch`/`finally` for `try_catch`);
|
|
366
|
-
`s.group(body)` and `s.util.post_process(body)` take it **positionally**.
|
|
367
|
-
`s.for` is **count-bounded** (`{ as, count: <Value>, body }`), not from/to. See the
|
|
368
|
-
authored signatures in `llms/statements-data.md`.
|
|
369
|
-
- **MCP servers & agents are distinct root kinds** that both persist under the
|
|
370
|
-
`toolset` payload key (so a same-name pair collides). `mcpServer({...})` exposes
|
|
371
|
-
tools over MCP (auth is per-tool — no server-level gate); `agent({...})` carries a
|
|
372
|
-
typed `llm` block — and so may an `mcpServer`, since the two are ONE stored
|
|
373
|
-
object distinguished by `type`. Their `tools` take a `tool()` handle (or name), resolved to the
|
|
374
|
-
tool's guid like the call family; a raw numeric `id` is an escape hatch.
|
|
375
|
-
- **`task.schedule` is an array** of `{ startsOn, freq?, repeatEnabled?, endsOn?, endsEnabled? }`
|
|
376
|
-
(`ScheduleDef[]`), not a single `{ type, value }`. `freq` is seconds; `startsOn`/
|
|
377
|
-
`endsOn` are timestamp strings validated at encode time — `"2026-01-01T00:00:00Z"`, or
|
|
378
|
-
the space-separated `"2026-01-01 00:00:00+0000"` a pulled workspace carries. Never
|
|
379
|
-
epoch numbers, and never zoneless: the engine stores the string verbatim without
|
|
380
|
-
checking it, so one it cannot parse deploys clean and the task then never fires.
|
|
381
|
-
- **`get_input`/`get_raw_input` read the whole payload**, not one named input
|
|
382
|
-
(args are `{ as?, encoding?, excludeMiddleware? }` — no `name`). For a single
|
|
383
|
-
input use `inp("name")`.
|
|
384
|
-
- **`f.password()` defaults to `access: "internal"`, so `db.get` does NOT return it.**
|
|
385
|
-
A login stack that reads `ref("u.password")` after a plain `db.get` fails at runtime
|
|
386
|
-
with `Unable to locate var: u.password` — the column is simply absent from the row.
|
|
387
|
-
Name it in the read's `output` to pull it: `s.db.get({ table: users, fieldName: "email",
|
|
388
|
-
fieldValue: inp("email"), output: ["id", "email", "password"], as: "u" })`, then
|
|
389
|
-
`s.security.check_password`. `output` OVERRIDES column visibility — it is the only way to
|
|
390
|
-
read an `internal` column, and `export()` warns when a stack reads one a `db.get` did not
|
|
391
|
-
return.
|
|
392
|
-
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
393
|
-
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
394
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped, and the
|
|
395
|
-
ARGUMENT is the subject. A bare `c.text("^…$")` is an invalid pattern that matches
|
|
396
|
-
*nothing* for every input, so a precondition on it silently rejects all values.
|
|
397
|
-
`c.regex(body, "i")` wraps + escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
|
|
398
|
-
Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
|
|
399
|
-
answers false for EVERY input, so an `if (matches) reject` guard admits what it
|
|
400
|
-
refuses. `withFilters` throws on a bare `c.text` pattern from ANY position in
|
|
401
|
-
the chain (a normalizer in front of the regex filter is refused too; nothing upstream
|
|
402
|
-
adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match`
|
|
403
|
-
is the same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
|
|
404
|
-
`export()` warns on a reversed pair in stored bytes. Better still:
|
|
405
|
-
a native typed input (`input.email`) over hand-rolled validation.
|
|
406
|
-
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
407
|
-
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
408
|
-
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
|
409
|
-
folded) and is refused past 128 levels. Mixed joins: `and(or(...anyOf), ...allOf)`.
|
|
410
|
-
- **Declare inputs with `input.<type>()`, read them with `inp("name")`.** `inp()`
|
|
411
|
-
resolves ONLY against this def's own `input` block — a value produced earlier in
|
|
412
|
-
the stack is `ref("var.field")`, not `inp("field")`. An undeclared name fails at
|
|
413
|
-
runtime with `Unable to locate input` (`export()` warns).
|
|
414
|
-
- **Don't take a password through `input.password` on login — it double-hashes.**
|
|
415
|
-
An `f.password()` column hashes on write, and `input.password` *also* hashes the
|
|
416
|
-
submission on bind, so `s.security.check_password` compares two different hashes
|
|
417
|
-
and a correct password always fails (`ok:false` on a found row). Take the submitted
|
|
418
|
-
password as `input.text()` on both signup and login and pass the plaintext straight
|
|
419
|
-
to `check_password` (which does the comparison hash itself). `export()` warns.
|
|
420
364
|
- **Agents authenticate with env vars — never `xanots login`.** `login` blocks on a
|
|
421
|
-
browser consent no agent can complete. Set
|
|
422
|
-
|
|
423
|
-
survives repeated runs.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
"instance_base_url": …, "workspace_id": <n>,
|
|
428
|
-
|
|
365
|
+
browser consent no agent can complete. Set all three of `$XANO_INSTANCE_URL`,
|
|
366
|
+
`$XANO_WORKSPACE_ID`, `$XANO_META_TOKEN` and run `deploy`/`release` directly: no disk, no
|
|
367
|
+
rotation, so it survives repeated runs.
|
|
368
|
+
- The triple outranks every other credential (`--config` and `$XANO_REFRESH_TOKEN` included,
|
|
369
|
+
each named on stderr when displaced). Setting SOME of the three is a hard error, so a
|
|
370
|
+
misspelled secret cannot fall through to whatever credential the runner has.
|
|
371
|
+
- As a file: `{ "type": "token", "instance_base_url": …, "workspace_id": <n>,
|
|
372
|
+
"meta_api_token": … }`. The older `$XANO_REFRESH_TOKEN` + `$XANO_CLIENT_ID` pair still
|
|
373
|
+
works but ROTATES: single-use.
|
|
429
374
|
- **Event-driven objects fire on an EPHEMERAL.** A `task` (scheduled), an `mcpServer`,
|
|
430
375
|
and every trigger — `tableTrigger` included — run normally on an ephemeral env, which
|
|
431
376
|
is where `deploy` sends them. So test an event-driven design (screen-on-insert, cron
|
|
432
377
|
cleanup, MCP tool call) by deploying it and letting it run.
|
|
433
|
-
- **Zero-based numeric keys make `c.obj` a LIST.** `c.obj({ "0": "a" })` evaluates to
|
|
434
|
-
`["a"]`: a numeric key IS an index in the engine's data model, so keys that are exactly
|
|
435
|
-
`0..n-1` come back as a list with HTTP 200 and no error. Write `c.array([...])` when you
|
|
436
|
-
want the list, or prefix the keys (`"k0"`) when you want an object. A non-zero-based
|
|
437
|
-
numeric key (`{ "2": … }`) survives as a key. Export warns.
|
|
438
|
-
- **To match every row, omit `where` — never pass a constant.** `where: c.bool(true)` is
|
|
439
|
-
not "no filter": a bare scalar literal states no condition and the engine reads the
|
|
440
|
-
resulting `context.search` as garbage, so it throws. An absent `where` is how "every
|
|
441
|
-
row" is spelled. A raw `Value` `where` stays the escape hatch for a clause built
|
|
442
|
-
elsewhere — `inp("clause")`, `ref("built_where")`, or `c.text("id > 0")`.
|
|
443
|
-
- **An `auth()`-keyed limiter collapses on any host with no caller identity — at every**
|
|
444
|
-
**tier.** `s.redis.ratelimit({ key: withFilters(c.text("rl:"), fl.concat(auth("id"))) })`
|
|
445
|
-
is the per-user form, but `auth()` is `null` on a public query, a task, or a function,
|
|
446
|
-
and every caller then shares one bucket. Attaching it once at
|
|
447
|
-
`apiGroup({ middleware })` or the workspace tier is the tempting DRY move and inherits
|
|
448
|
-
the same collapse onto every member endpoint. Export warns, naming the tier.
|
|
449
378
|
|
|
450
379
|
## Statements
|
|
451
380
|
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xanots",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.",
|
|
5
5
|
"coverage": {
|
|
6
6
|
"objectKinds": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
{
|
|
78
78
|
"name": "c.obj",
|
|
79
79
|
"signature": "(o?: Json | null) => Value",
|
|
80
|
-
"description": "Object constant → tag \"const:obj\". A populated one stores an empty {} carrying one `set` filter per key — 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.\"). ⚠ 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\":…} survives as a key) — that is the platform, not this encoding. No argument = the empty object {} — 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 — a nested tagged value (inp/ref/auth/c.*) is rejected; for a computed object response use a record of values, not c.obj."
|
|
80
|
+
"description": "Object constant → tag \"const:obj\". A populated one stores an empty {} carrying one `set` filter per key — 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.\"). ⚠ 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\":…} survives as a key) — 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 {} — 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 — a nested tagged value (inp/ref/auth/c.*) is rejected; for a computed object response use a record of values, not c.obj."
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "c.array",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
{
|
|
104
104
|
"name": "obj",
|
|
105
105
|
"signature": "(fields: Record<string, Value | nested>) => ObjValue<typeof fields>",
|
|
106
|
-
"description": "Dynamic object value → tag \"const:expr2\" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:\"a.b\"`.
|
|
106
|
+
"description": "Dynamic object value → tag \"const:expr2\" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:\"a.b\"`. 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 — `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([...])` — 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 {}) — 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 — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args."
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"name": "ref",
|
|
@@ -309,7 +309,7 @@
|
|
|
309
309
|
"kind": "workflow_test",
|
|
310
310
|
"payloadKey": "workflow_test",
|
|
311
311
|
"authorFactory": "workflowTest",
|
|
312
|
-
"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
|
|
312
|
+
"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`.",
|
|
313
313
|
"registerMethod": "registerWorkflowTests",
|
|
314
314
|
"registered": true
|
|
315
315
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xanots/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "XanoTS — your Xano backend as TypeScript. `xanots deploy` ships your typed workspace (and an optional static frontend) to a live, auto-expiring ephemeral environment and prints its URL. init → deploy → URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"xanots",
|