@xanots/sdk 0.0.10 → 0.0.12
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 +33 -0
- package/README.md +48 -5
- package/dist/.build-fingerprint +1 -1
- package/dist/{agent-file-refresh-QNKN5RYD.js → agent-file-refresh-GQWAAOBV.js} +4 -4
- package/dist/bin.js +12 -11
- package/dist/{branch-commands-2BLOC2GR.js → branch-commands-5KMPAAZV.js} +7 -7
- package/dist/bundle.d.ts +218 -0
- package/dist/bundle.js +143 -0
- package/dist/{capture-4WVJY4DQ.js → capture-YLUVAITI.js} +2 -2
- package/dist/chunk-2VTJSI6X.js +192 -0
- package/dist/{chunk-5XZ744TS.js → chunk-2ZCUO2UG.js} +1 -1
- package/dist/{chunk-OYMR5AMJ.js → chunk-3LQGF2WS.js} +2 -2
- package/dist/{chunk-WP4OZZV4.js → chunk-3VFCKHOB.js} +2 -2
- package/dist/{chunk-XHEXOES3.js → chunk-5YDINUAP.js} +1 -1
- package/dist/{chunk-LU7TRWMC.js → chunk-6VNRMKFJ.js} +2 -2
- package/dist/{chunk-CTD5ZCV6.js → chunk-7WRJPKGK.js} +2 -2
- package/dist/{chunk-3INK4Y4E.js → chunk-AE3PDSDS.js} +1 -1
- package/dist/{chunk-5YCQ2QHH.js → chunk-AG5GCZDD.js} +2 -2
- package/dist/{chunk-DBFU47BJ.js → chunk-AOFFKSJC.js} +2 -2
- package/dist/{chunk-TCFIPDB3.js → chunk-AVGDL6RB.js} +1 -1
- package/dist/{chunk-4BXJGVZ3.js → chunk-DCMANKMX.js} +2 -69
- package/dist/{chunk-WHOJWOSV.js → chunk-EETVJZAZ.js} +1 -1
- package/dist/chunk-EHP3WPEG.js +21 -0
- package/dist/{chunk-W24FJHPD.js → chunk-ELK7UALJ.js} +3 -3
- package/dist/{chunk-VNQM3V2C.js → chunk-EXENFOWE.js} +2 -2
- package/dist/{chunk-P6TAVLOX.js → chunk-GSP2BY4F.js} +17 -10
- package/dist/{chunk-AIZKXUNP.js → chunk-IMLYGQK6.js} +2 -2
- package/dist/{chunk-LBYWGMOA.js → chunk-N74KDCBD.js} +176 -42
- package/dist/{chunk-3IGNIP6R.js → chunk-NDP7OUPS.js} +1 -1
- package/dist/chunk-OHX6MIUZ.js +184 -0
- package/dist/{chunk-EQW3YT5U.js → chunk-P6PVBQL6.js} +2 -2
- package/dist/{chunk-I7DQDJAM.js → chunk-PLE5QQOZ.js} +55 -56
- package/dist/chunk-PR7OXHGZ.js +70 -0
- package/dist/{chunk-VKSOTZK3.js → chunk-QYSQ3UDO.js} +160 -8
- package/dist/{chunk-ZQ2PKR6R.js → chunk-RQ3FXV4K.js} +2 -2
- package/dist/{chunk-Q77KNEUL.js → chunk-RQNMTDXD.js} +1703 -2
- package/dist/{chunk-QKM4U5UK.js → chunk-TJS2AF5Y.js} +2 -2
- package/dist/{chunk-DIA7CT7J.js → chunk-V5Y7D4LH.js} +11 -2
- package/dist/{chunk-KA6G2L7U.js → chunk-VK26K7AY.js} +3 -3
- package/dist/{chunk-YYFXVYPX.js → chunk-VPAWRBK5.js} +12 -16
- package/dist/{chunk-5R73LFWK.js → chunk-VRNZ2NVV.js} +7 -7
- package/dist/{chunk-ACCBOMCB.js → chunk-XT3XQ4PF.js} +32 -27
- package/dist/{chunk-7JDT4PBU.js → chunk-XWFRNJMQ.js} +5 -179
- package/dist/{chunk-7ZYW652H.js → chunk-YHS6VVLJ.js} +2 -2
- package/dist/{chunk-75Z74TA7.js → chunk-YX22LKQE.js} +2 -2
- package/dist/{chunk-F6CYJ7TN.js → chunk-Z2ZIE5CO.js} +2 -2
- package/dist/cli.d.ts +13 -6
- package/dist/cli.js +11 -10
- package/dist/codegen-command-Y2SPMAUW.js +47 -0
- package/dist/codegen.d.ts +5 -4
- package/dist/codegen.js +2 -2
- package/dist/{completion-WF46272M.js → completion-HJU5QEFB.js} +2 -2
- package/dist/{deploy-command-QTIVA22A.js → deploy-command-XHS5PKPV.js} +20 -19
- package/dist/{ephemeral-command-U4AQ3TXX.js → ephemeral-command-2NKPEXUU.js} +8 -8
- package/dist/index.d.ts +88 -102
- package/dist/index.js +13 -12
- package/dist/init-command-23FFNUFT.js +32 -0
- package/dist/internal.d.ts +10 -16
- package/dist/internal.js +76 -54
- package/dist/io-UBDMMDH6.js +12 -0
- package/dist/{live-diff-IXKBVG4K.js → live-diff-HCOTN5WC.js} +3 -3
- package/dist/{lock-GFXD6G2E.js → lock-HQ4KARU2.js} +3 -2
- package/dist/{lock-commands-DQ7CUMIV.js → lock-commands-WCRC56ME.js} +12 -11
- package/dist/{login-command-Z6CHTA57.js → login-command-P7LXD5TE.js} +6 -6
- package/dist/{loop-D5NPL4VH.js → loop-IC5ISSYB.js} +3 -3
- package/dist/{marketplace-command-P4IPLJ6J.js → marketplace-command-T6JCHW7J.js} +4 -4
- package/dist/{meta-client-K2J4XH64.js → meta-client-LKRKR3L2.js} +4 -4
- package/dist/node.d.ts +6 -5
- package/dist/node.js +18 -17
- package/dist/{preflight-command-GZ2GE5RN.js → preflight-command-TZWPHBXY.js} +17 -16
- package/dist/{profile-command-LJSBDV2L.js → profile-command-WBNSMQSI.js} +3 -3
- package/dist/{release-command-YUBTNHVX.js → release-command-WEFYRTCI.js} +27 -26
- package/dist/response-D6xGLEIn.d.ts +839 -0
- package/dist/{routes-manifest-PWZHDOI5.js → routes-manifest-5ZFKUQWA.js} +2 -2
- package/dist/{runtime-V4C3AC3A.js → runtime-LSLIDALK.js} +1 -1
- package/dist/scaffold.d.ts +16 -8
- package/dist/scaffold.js +7 -11
- package/dist/{static-host-3WMV7IZO.js → static-host-4JDQCHUW.js} +1 -1
- package/dist/{status-command-AL47VG7H.js → status-command-VARULAR5.js} +4 -4
- package/dist/{store-BG1UPZ3Z.d.ts → store-DAnUIi1T.d.ts} +95 -87
- package/dist/{test-command-YAZLKLGQ.js → test-command-YCAR4O35.js} +7 -7
- package/dist/{upgrade-command-FB5QJ363.js → upgrade-command-GYEIMJBG.js} +16 -16
- package/dist/{workspace-2COHDBM3.js → workspace-33KHFLX3.js} +2 -2
- package/dist/{workspace-command-43P42FBP.js → workspace-command-2ZTGT26W.js} +28 -27
- package/dist/{workspace-export-DURY5WYL.js → workspace-export-MLYTYZS5.js} +3 -3
- package/dist/{response-CVAE2kMj.d.ts → xdo-ODuJklk6.d.ts} +28 -860
- package/guides/authoring.md +11 -2
- package/guides/typed-frontend.md +5 -5
- package/llms/filters.md +11 -10
- package/llms/kinds-core.md +1 -1
- package/llms/statements-data.md +6 -3
- package/llms/values.md +1 -1
- package/llms-full.txt +53 -42
- package/llms.txt +34 -27
- package/manifest.json +6 -2
- package/package.json +7 -2
- package/dist/chunk-ANUDXFEX.js +0 -881
- package/dist/chunk-JGCWTCA7.js +0 -95
- package/dist/chunk-YBC3IKMF.js +0 -845
- package/dist/codegen-command-GB3H2KQ7.js +0 -46
- package/dist/init-command-MAXULNAD.js +0 -33
- package/dist/io-M7XZEMK7.js +0 -11
package/guides/authoring.md
CHANGED
|
@@ -137,6 +137,13 @@ auto-assigned `id`/`created_at`, `s.db.del` binds `null`, and `edit`/`del` **thr
|
|
|
137
137
|
the engine reads the operand as a text literal and fails at runtime with a parse error
|
|
138
138
|
naming the *other* operand, so `db.query` rejects that spelling at export instead.
|
|
139
139
|
|
|
140
|
+
- **A join adds no columns to the returned row.** With or without a `bind`, a row is the
|
|
141
|
+
queried table's columns — which is what `InferResponse` types, so there is no `row.author`
|
|
142
|
+
to read. To bring a joined column back, project it with an `eval` whose `name` is the dotted
|
|
143
|
+
path: `eval: [{ name: "author.name", as: "author_name" }]` puts `author_name` on the row and
|
|
144
|
+
on the inferred type. A *bare* name there fails at runtime (it qualifies to the base table),
|
|
145
|
+
and a dotted joined column in `output` is dropped with no error.
|
|
146
|
+
|
|
140
147
|
- **Paging changes the response shape.** Supplying `paging` with metadata on (the default)
|
|
141
148
|
returns a **paging envelope** — `{ items, curPage, nextPage, prevPage, offset, perPage,
|
|
142
149
|
itemsReceived }`, plus totals when `totals: true` — instead of a bare `Row[]`, and
|
|
@@ -194,8 +201,10 @@ auto-assigned `id`/`created_at`, `s.db.del` binds `null`, and `edit`/`del` **thr
|
|
|
194
201
|
`lower`, and the `epochms_add_day` / `epochms_sub_month` family). The request-time timestamp
|
|
195
202
|
filters do not — `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`,
|
|
196
203
|
`epochms_from_format` — and the engine doesn't degrade gracefully: the request dies with a
|
|
197
|
-
bare fatal naming nothing. For a relative cutoff, use the SQL-side spelling
|
|
198
|
-
|
|
204
|
+
bare fatal naming nothing. For a relative cutoff, use the SQL-side spelling — by raw name,
|
|
205
|
+
since `fl.*` carries no builder for that family: `filter("epochms_add_day", …)` or the raw
|
|
206
|
+
`{ name, arg }` form — or compute it in an earlier `s.set_var` and `ref()` that. `export()`
|
|
207
|
+
warns; a bare `c.now()` is always fine.
|
|
199
208
|
- **A `s.switch` case without `break: true` falls through.** The engine's default is
|
|
200
209
|
fallthrough, so a matched case also runs every *later* case body — and the `default` block
|
|
201
210
|
too. Whatever those bodies write gets written two or three times, at HTTP 200, with `tsc`
|
package/guides/typed-frontend.md
CHANGED
|
@@ -94,7 +94,7 @@ on the query and every caller derives from that single source of truth:
|
|
|
94
94
|
|
|
95
95
|
```ts
|
|
96
96
|
const getPost = query({
|
|
97
|
-
verb: "GET", apiGroup: blog, name: "get_post",
|
|
97
|
+
verb: "GET", apiGroup: blog, name: "get_post/{id}", // one row → address it in the path
|
|
98
98
|
input: { id: input.int({ required: true }) },
|
|
99
99
|
stack: [s.db.query({ table: post, where: expr(col("id"), "=", inp("id")), as: "rows" })],
|
|
100
100
|
// A filtered response is opaque to the static walk, so derivation is `unknown`.
|
|
@@ -157,10 +157,10 @@ also pulls whatever its `stack` builds — the `s.*`/`c.*` factory *calls* run a
|
|
|
157
157
|
to construct the def, so they can't be tree-shaken out. Types are free (`InferInput`/
|
|
158
158
|
`InferRow` erase to nothing — use `import type`). That cost is a **floor**, not a function of
|
|
159
159
|
how lean the def is. Measured on a Vite lib build against the published package: one
|
|
160
|
-
`apiGroup` + one empty `query`, imported for a single `getPath()`, is **
|
|
161
|
-
(
|
|
160
|
+
`apiGroup` + one empty `query`, imported for a single `getPath()`, is **267 kB minified
|
|
161
|
+
(65 kB gzipped)** against 56 B for a hand-written path string. A realistic def — two
|
|
162
162
|
tables, a foreign key, a typed input, a `db.query` with a `where` and a sort, plus a
|
|
163
|
-
second endpoint — measures
|
|
163
|
+
second endpoint — measures 269 kB. That 2 kB spread is the point: the floor is the SDK
|
|
164
164
|
runtime itself, so splitting modules or simplifying a def does not move it, and the cost
|
|
165
165
|
is paid by importing any def at all.
|
|
166
166
|
|
|
@@ -172,7 +172,7 @@ xanots routes ./xano/index.ts --emit xano/routes.gen.ts
|
|
|
172
172
|
```
|
|
173
173
|
|
|
174
174
|
The emitted file is plain data plus one interpolator and imports nothing at all — the same
|
|
175
|
-
app builds to 1.3 kB, a
|
|
175
|
+
app builds to 1.3 kB, a ~200x saving, with no SDK code in the output. Route names and their `{param}` keys are still checked at compile
|
|
176
176
|
time, so a backend rename is a compile error rather than a 404:
|
|
177
177
|
|
|
178
178
|
```ts
|
package/llms/filters.md
CHANGED
|
@@ -39,12 +39,13 @@ database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHER
|
|
|
39
39
|
which the DB applies atomically. Reserve the pipeline form for low-contention counters
|
|
40
40
|
where a rare lost update is acceptable.
|
|
41
41
|
⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
|
|
42
|
-
does NOT expose: the engine derives
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
the
|
|
47
|
-
|
|
42
|
+
does NOT expose: the engine derives it from ids assigned at import — not knowable from a
|
|
43
|
+
`table()` def (identity is a name + guid, not the numeric id) — and `sql_name` persists
|
|
44
|
+
empty. The derived name is also NOT STABLE: a deploy is a full replace, so every table is
|
|
45
|
+
created afresh and the id in its name moves each time, on the same unchanged project. So
|
|
46
|
+
the safe counter drops out of the typed surface: resolve the physical name from
|
|
47
|
+
`information_schema` inside the request that uses it, and never store, cache or hardcode
|
|
48
|
+
one. A typed atomic path needs an engine change.
|
|
48
49
|
|
|
49
50
|
- `fl.add(value: decimal): decimal`
|
|
50
51
|
- `fl.append(value: <T>, path: text): <T>[]`
|
|
@@ -136,11 +137,11 @@ engine change.
|
|
|
136
137
|
- `fl.prepend(value: <T>, path: text): <T>[]`
|
|
137
138
|
- `fl.range(start: int, stop: int): int[]`
|
|
138
139
|
- `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
|
|
139
|
-
- `fl.regex_match(subject: text): text[]`
|
|
140
|
-
- `fl.regex_match_all(subject: text): text[]`
|
|
140
|
+
- `fl.regex_match(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
|
|
141
|
+
- `fl.regex_match_all(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
|
|
141
142
|
- `fl.regex_quote(delimiter?: text): text`
|
|
142
|
-
- `fl.regex_replace(replacement: text, subject: text): text`
|
|
143
|
-
- `fl.regex_test(subject: text): bool`
|
|
143
|
+
- `fl.regex_replace(replacement: text, subject: text): text` — piped value is the PATTERN, `subject` is the text searched — see `regex_test`. The replacement comes FIRST
|
|
144
|
+
- `fl.regex_test(subject: text): bool` — piped value is the PATTERN (build it with `c.regex(...)`); the arg is the subject — the REVERSE of `contains`/`starts_with`. Swapped, it answers false for every input with no error, so write `withFilters(c.regex("^a+$"), fl.regex_test(inp("s")))` (or name the arg: `fl.regex_test({ subject: inp("s") })`). A pattern found in the subject slot is refused at build time
|
|
144
145
|
- `fl.round(precision?: int): decimal`
|
|
145
146
|
- `fl.rtrim(mask?: text): text`
|
|
146
147
|
- `fl.secureid_decode(salt: text): int`
|
package/llms/kinds-core.md
CHANGED
|
@@ -13,7 +13,7 @@ to survive a rename).
|
|
|
13
13
|
- `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
|
|
14
14
|
- `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
|
|
15
15
|
- `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
|
|
16
|
-
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
16
|
+
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param. Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
17
17
|
- **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
|
|
18
18
|
- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
|
|
19
19
|
- `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
|
package/llms/statements-data.md
CHANGED
|
@@ -12,7 +12,7 @@ primary key `id`):
|
|
|
12
12
|
|
|
13
13
|
- `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` — one row by field match; `output` restricts returned columns (and overrides column visibility — it can pull `internal` columns like a password hash).
|
|
14
14
|
- `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` for an id that names no row. Both spellings are live in pulled workspaces.
|
|
15
|
-
- ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`.
|
|
15
|
+
- ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`. `export()` warns when the `id` is statically a `0` — the literal `c.int(0)`, an `inp()` whose declared input default is `0`, or a `ref()` to a column declared `default: 0` — and `--strict` fails the build.
|
|
16
16
|
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
17
17
|
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
18
18
|
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
@@ -34,13 +34,14 @@ primary key `id`):
|
|
|
34
34
|
- ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
|
|
35
35
|
- Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
|
|
36
36
|
- An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
|
|
37
|
-
- ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
|
|
37
|
+
- ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) — by raw name, since `fl.*` has no builder for them — or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
|
|
38
38
|
- `bind: [{ table, as?, join?, where? }]` — joins (`context.bind[]`). `join` defaults to `"inner"`. `as` defaults to the table name; two joins to the same table need distinct aliases.
|
|
39
39
|
- ⚠ In `where`/`sort`/`eval` a JOINED column takes a dotted path (`col("team_row.id")`); THIS query's own columns stay **bare** (`col("team")`). Qualifying your own by table name needs `tableAlias` (same rule as `aggregate`) — without it the engine reads the operand as text and 400s `ParseError: Invalid value for param` naming the OTHER operand, so it throws at export instead.
|
|
40
40
|
- `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`
|
|
41
|
+
- ⚠ A join does NOT put the joined table's columns on the returned row — with or without a `bind`, a row is the QUERIED table's columns, which is what `InferResponse` types. There is no `row.team_row`. To read a joined column, PROJECT it with an `eval` whose `name` is the dotted path: `eval: [{ name: "team_row.name", as: "team_name" }]` puts `team_name` on the row and on the inferred type. A bare `name` there is `Unsupported parameter reference` at runtime (it qualifies to the base table), and a dotted joined column in `output` is dropped with no error.
|
|
41
42
|
- `returnType` — `"list"` (default) | `"single"` | `"count"` | `"exists"` | `"stream"` | `"aggregate"`. Drives `context.return.type` AND the `InferResponse` shape: `count`→`number`, `exists`→`boolean`, `single`→`Row|null`, `stream`→`Row[]` (pageable, no envelope), `list`→`Row[]`/envelope, `aggregate`→rows keyed by the `aggregate.group`/`eval` aliases. ⚠ A bare `count` of ZERO serializes as an EMPTY body, not `0` — a client parsing JSON gets a parse error on the one result it most needs to handle. Wrap it: `response: { count: ref("n") }`.
|
|
42
43
|
- `eval: [{ name, as, filters? }]` — computed columns (`context.eval[]`). Each `as` grafts onto the row as an `unknown` key in `InferResponse`; shadowing a real column throws. Write `name` **bare** (`"embedding"`) — it is alias-qualified on emit exactly like `aggregate` (a bare eval name is `Unsupported param format` at runtime), and the statement declares the alias it used. An `as` alias is `sort`able in the SAME query.
|
|
43
|
-
- An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators.
|
|
44
|
+
- An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators. ⚠ `fl.*` carries NO builder for these — reach them by raw name, `filter("epochms_add_day", …)` or the `{ name, arg }` form. The name lists are `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS` on `@xanots/sdk/internal`.
|
|
44
45
|
- **Vector similarity search** — the ONLY way to query an `f.vector` column (no `SearchOp` does distance). `eval: [{ name: "embedding", as: "distance", filters: [{ name: "vector_cos_distance", arg: [inp("q")] }] }]` + `sort: [{ sortBy: "distance", dir: "asc" }]` ranks in the DATABASE over the column's index. Match the filter to the index `op` (`vector_cos_distance`↔`vector_cosine_ops`, `vector_l2_distance`↔`vector_l2_ops`, `vector_l1_distance`↔`vector_l1_ops`, `vector_inner_product`↔`vector_ip_ops`); `vector_cos_similarity` is the inverse, so sort it `desc`. The same filter on a `where` operand cuts off BY distance instead of by row count.
|
|
45
46
|
- `aggregate: { group?, eval?, sort?, paging? }` (with `returnType:"aggregate"`) builds `context.return.aggregate`. `group`/`eval` are `{ name, as, filters? }`, an aggregator like `sum`/`count` riding `filters`. Some aggregators resolve ONLY here, not in a runtime value pipeline: `count_distinct`, `median`, `to_list`/`to_distinct_list` (each with `_asc`/`_desc`), and `vector_distance`.
|
|
46
47
|
- ⚠ Write each `name` as a **bare** column (`"status"`). It is alias-qualified to `"<alias>.status"` on emit — the engine rejects an unqualified column in an aggregate with `Unsupported param format`. An already-dotted `name` (a `bind`ed/joined column) passes through.
|
|
@@ -54,6 +55,8 @@ primary key `id`):
|
|
|
54
55
|
- `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
|
|
55
56
|
- `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
|
|
56
57
|
- `s.db.direct_query({ sql, responseType?, args?, parser?, as? })` — `sql` is a **raw string** (not a `Value`); binds go in `args: Value[]`. `parser: "template_engine"` renders the body as a template first — how a query interpolates a column or table name a bound arg cannot carry; omit it for the default.
|
|
58
|
+
- Template placeholders are Twig over the request scope: `{{ $input.name }}` for an input, `{{ $var.name }}` for a stack variable. ⚠ A BARE `{{ name }}` renders as the empty string — HTTP 200, no error, a query that silently ran with a blank where the value belonged. A bound `?` arg carries a VALUE without the template at all.
|
|
59
|
+
- ⚠ A table's PHYSICAL name is **not stable across deploys**. A deploy is a full replace, so every table is created afresh and the id in its name moves every time — the same unchanged project redeployed three times gave one table three different names. Never store, cache, hardcode or fixture one: resolve it from `information_schema` inside the same request that uses it.
|
|
57
60
|
- `s.db.external.<engine>.direct_query({ sql, connectionString, responseType?, args?, parser?, as? })` — same shape against an EXTERNAL database; `<engine>` is `postgres`/`mysql`/`mssql`/`oracle`/`snowflake`. `connectionString` is a `Value` — reach for `env(...)`, not a literal — stored as `context.connection_string_flex`. A bare string stores the older `context.connection_string` instead (an env-var name unless it looks like a URL); each form round-trips as itself.
|
|
58
61
|
- `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
|
|
59
62
|
- `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` — `items` is an array `Value`.
|
package/llms/values.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- `c.array(a: Json[]) => Value` — Array constant (JSON string) → tag "const:array". Plain JSON literals only — a nested tagged value is rejected, same as c.obj.
|
|
13
13
|
- `c.expression(source: string) => Value` — Xano Expression Engine source, passed through VERBATIM → tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). ⚠️ NOT VALIDATED — never parsed or type-checked, invisible to InferResponse, and untouched by a rename that updates every typed ref(); a typo surfaces at runtime or as a wrong answer. Use it ONLY for syntax the typed surfaces cannot express (~ concatenation, inline arithmetic, conditionals) — prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.
|
|
14
14
|
- `c.now() => Value` — Current time as epoch-ms — the engine-native const:epochms constant (no filter). Valid inline as a where/cmp operand. For cutoff math (cutoff = now - max_age) either compare inline or, for reuse/readability, hoist it into an s.set_var and compare against the var.
|
|
15
|
-
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. Still rejected: a filter ARGUMENT
|
|
15
|
+
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — 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.
|
|
16
16
|
- `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.
|
|
17
17
|
- `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it — an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).
|
|
18
18
|
- `col(name: string) => Value` — Reference a table column → tag "col".
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.12
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -16,25 +16,26 @@ caller, `c.*` a constant — resolved at request time; JS operators over them do
|
|
|
16
16
|
compute (see Gotchas). Requests share no memory — state persists in tables or redis.
|
|
17
17
|
|
|
18
18
|
Coverage: object kinds 25/31, statement surfaces 214/214, filters 226 (226 typed).
|
|
19
|
-
Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull;
|
|
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
22
|
every gotcha, and control flow. Per-surface detail lives in the topic files listed
|
|
23
|
-
below — open the one whose condition matches the task,
|
|
24
|
-
exhaustive per-entry detail in NEITHER — a statement's
|
|
25
|
-
defaults, a filter's
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
below — open the one whose condition matches the task, skip the rest. For
|
|
24
|
+
exhaustive per-entry detail in NEITHER — a statement's field schema with engine
|
|
25
|
+
defaults, a filter's full argument list, the `storedName` mapping — do a TARGETED
|
|
26
|
+
lookup in the shipped `manifest.json` (a program imports it as
|
|
27
|
+
`@xanots/sdk/manifest.json`, needing `with { type: "json" }` in Node ESM;
|
|
28
|
+
it is ~60k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
|
|
28
29
|
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
29
30
|
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
31
|
+
⚠ `fields` is null on the 65 `declarative: false` statements (`db.get`, …): typed wrappers whose arguments are the factory's `.d.ts` signature. Null ≠ missing.
|
|
30
32
|
Select a statement on `sPath` (the `s.*` path you write), NOT `surface` (the XanoScript term): 24 of 214 differ — `var`→`set_var`, `break`→`foreach_break`, `foreach.remove`→`foreach_remove`, and every `expect.*`.
|
|
31
33
|
|
|
32
34
|
## Topic files
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`llms-full.txt` is everything concatenated — one fetch, for a reader that cannot open files.
|
|
36
|
+
Paths are relative to this file (`node_modules/@xanots/sdk/` once installed), so a
|
|
37
|
+
plain file read resolves them at the version you have.
|
|
38
|
+
`llms-full.txt` is everything concatenated — one fetch for a reader that cannot open files.
|
|
38
39
|
|
|
39
40
|
- [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — every authorable primitive (tables, endpoints, functions, tasks, agents, MCP, realtime, microservices, …), what each is, and which factory + register method build it. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
|
|
40
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.
|
|
@@ -266,6 +267,10 @@ Non-obvious authoring rules:
|
|
|
266
267
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
268
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
269
|
check to the client.
|
|
270
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
271
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
272
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
273
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
269
274
|
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
275
|
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
276
|
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
@@ -320,18 +325,19 @@ Non-obvious authoring rules:
|
|
|
320
325
|
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
321
326
|
either way; only `deploy` ships them. The `node:fs` writers
|
|
322
327
|
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
323
|
-
NOT the browser-safe `@xanots/sdk` entry (
|
|
324
|
-
|
|
328
|
+
NOT the browser-safe `@xanots/sdk` entry (a frontend imports defs from it for
|
|
329
|
+
`getPath()`/`InferInput`).
|
|
325
330
|
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
326
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`.
|
|
327
334
|
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
328
335
|
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
329
336
|
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
330
337
|
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
331
|
-
⚠ A FLOOR — **~
|
|
338
|
+
⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
|
|
332
339
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
|
-
one, adds ~
|
|
334
|
-
def does will not reduce it.
|
|
340
|
+
one, adds ~2 kB — so reducing what a def does will not reduce it.
|
|
335
341
|
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
342
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
343
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
@@ -385,17 +391,18 @@ Non-obvious authoring rules:
|
|
|
385
391
|
return.
|
|
386
392
|
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
387
393
|
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
388
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped
|
|
389
|
-
`c.text("
|
|
390
|
-
input, so a precondition on it silently rejects all values
|
|
391
|
-
`c.regex(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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.
|
|
399
406
|
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
400
407
|
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
401
408
|
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
|
@@ -522,7 +529,7 @@ to survive a rename).
|
|
|
522
529
|
- `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
|
|
523
530
|
- `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
|
|
524
531
|
- `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
|
|
525
|
-
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
532
|
+
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param. Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
526
533
|
- **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
|
|
527
534
|
- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
|
|
528
535
|
- `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
|
|
@@ -785,7 +792,7 @@ primary key `id`):
|
|
|
785
792
|
|
|
786
793
|
- `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` — one row by field match; `output` restricts returned columns (and overrides column visibility — it can pull `internal` columns like a password hash).
|
|
787
794
|
- `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` for an id that names no row. Both spellings are live in pulled workspaces.
|
|
788
|
-
- ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`.
|
|
795
|
+
- ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`. `export()` warns when the `id` is statically a `0` — the literal `c.int(0)`, an `inp()` whose declared input default is `0`, or a `ref()` to a column declared `default: 0` — and `--strict` fails the build.
|
|
789
796
|
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
790
797
|
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
791
798
|
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
@@ -807,13 +814,14 @@ primary key `id`):
|
|
|
807
814
|
- ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
|
|
808
815
|
- Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
|
|
809
816
|
- An operand may be a bare value (`col`/`inp`/`ref`/`auth`/`c.*`) OR a **filtered** value (`withFilters(...)`) inline — the engine compiles the string and arithmetic filters (`trim`, `concat`, `upper`, `lower`, …) into the SQL.
|
|
810
|
-
- ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
|
|
817
|
+
- ⚠ The REQUEST-TIME timestamp filters have no SQL form and kill the request with a bare fatal naming nothing: `epochms_transform`, `epochms_add_ms`, `epochms_add_secs`, `epochms_date`, `epochms_from_format`. For a relative cutoff use the SQL-side family instead (`epochms_add_day`, `epochms_sub_month`, `epochms_year`, …) — by raw name, since `fl.*` has no builder for them — or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
|
|
811
818
|
- `bind: [{ table, as?, join?, where? }]` — joins (`context.bind[]`). `join` defaults to `"inner"`. `as` defaults to the table name; two joins to the same table need distinct aliases.
|
|
812
819
|
- ⚠ In `where`/`sort`/`eval` a JOINED column takes a dotted path (`col("team_row.id")`); THIS query's own columns stay **bare** (`col("team")`). Qualifying your own by table name needs `tableAlias` (same rule as `aggregate`) — without it the engine reads the operand as text and 400s `ParseError: Invalid value for param` naming the OTHER operand, so it throws at export instead.
|
|
813
820
|
- `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`
|
|
821
|
+
- ⚠ A join does NOT put the joined table's columns on the returned row — with or without a `bind`, a row is the QUERIED table's columns, which is what `InferResponse` types. There is no `row.team_row`. To read a joined column, PROJECT it with an `eval` whose `name` is the dotted path: `eval: [{ name: "team_row.name", as: "team_name" }]` puts `team_name` on the row and on the inferred type. A bare `name` there is `Unsupported parameter reference` at runtime (it qualifies to the base table), and a dotted joined column in `output` is dropped with no error.
|
|
814
822
|
- `returnType` — `"list"` (default) | `"single"` | `"count"` | `"exists"` | `"stream"` | `"aggregate"`. Drives `context.return.type` AND the `InferResponse` shape: `count`→`number`, `exists`→`boolean`, `single`→`Row|null`, `stream`→`Row[]` (pageable, no envelope), `list`→`Row[]`/envelope, `aggregate`→rows keyed by the `aggregate.group`/`eval` aliases. ⚠ A bare `count` of ZERO serializes as an EMPTY body, not `0` — a client parsing JSON gets a parse error on the one result it most needs to handle. Wrap it: `response: { count: ref("n") }`.
|
|
815
823
|
- `eval: [{ name, as, filters? }]` — computed columns (`context.eval[]`). Each `as` grafts onto the row as an `unknown` key in `InferResponse`; shadowing a real column throws. Write `name` **bare** (`"embedding"`) — it is alias-qualified on emit exactly like `aggregate` (a bare eval name is `Unsupported param format` at runtime), and the statement declares the alias it used. An `as` alias is `sort`able in the SAME query.
|
|
816
|
-
- An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators.
|
|
824
|
+
- An `eval`/`sort`/`where` filter pipeline compiles to **SQL**, so it resolves a DIFFERENT registry than `fl.*` (which runs in the request): the vector family, geo `distance`/`within`/`covers`, `search_rank`, the aggregators. ⚠ `fl.*` carries NO builder for these — reach them by raw name, `filter("epochms_add_day", …)` or the `{ name, arg }` form. The name lists are `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS` on `@xanots/sdk/internal`.
|
|
817
825
|
- **Vector similarity search** — the ONLY way to query an `f.vector` column (no `SearchOp` does distance). `eval: [{ name: "embedding", as: "distance", filters: [{ name: "vector_cos_distance", arg: [inp("q")] }] }]` + `sort: [{ sortBy: "distance", dir: "asc" }]` ranks in the DATABASE over the column's index. Match the filter to the index `op` (`vector_cos_distance`↔`vector_cosine_ops`, `vector_l2_distance`↔`vector_l2_ops`, `vector_l1_distance`↔`vector_l1_ops`, `vector_inner_product`↔`vector_ip_ops`); `vector_cos_similarity` is the inverse, so sort it `desc`. The same filter on a `where` operand cuts off BY distance instead of by row count.
|
|
818
826
|
- `aggregate: { group?, eval?, sort?, paging? }` (with `returnType:"aggregate"`) builds `context.return.aggregate`. `group`/`eval` are `{ name, as, filters? }`, an aggregator like `sum`/`count` riding `filters`. Some aggregators resolve ONLY here, not in a runtime value pipeline: `count_distinct`, `median`, `to_list`/`to_distinct_list` (each with `_asc`/`_desc`), and `vector_distance`.
|
|
819
827
|
- ⚠ Write each `name` as a **bare** column (`"status"`). It is alias-qualified to `"<alias>.status"` on emit — the engine rejects an unqualified column in an aggregate with `Unsupported param format`. An already-dotted `name` (a `bind`ed/joined column) passes through.
|
|
@@ -827,6 +835,8 @@ primary key `id`):
|
|
|
827
835
|
- `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
|
|
828
836
|
- `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
|
|
829
837
|
- `s.db.direct_query({ sql, responseType?, args?, parser?, as? })` — `sql` is a **raw string** (not a `Value`); binds go in `args: Value[]`. `parser: "template_engine"` renders the body as a template first — how a query interpolates a column or table name a bound arg cannot carry; omit it for the default.
|
|
838
|
+
- Template placeholders are Twig over the request scope: `{{ $input.name }}` for an input, `{{ $var.name }}` for a stack variable. ⚠ A BARE `{{ name }}` renders as the empty string — HTTP 200, no error, a query that silently ran with a blank where the value belonged. A bound `?` arg carries a VALUE without the template at all.
|
|
839
|
+
- ⚠ A table's PHYSICAL name is **not stable across deploys**. A deploy is a full replace, so every table is created afresh and the id in its name moves every time — the same unchanged project redeployed three times gave one table three different names. Never store, cache, hardcode or fixture one: resolve it from `information_schema` inside the same request that uses it.
|
|
830
840
|
- `s.db.external.<engine>.direct_query({ sql, connectionString, responseType?, args?, parser?, as? })` — same shape against an EXTERNAL database; `<engine>` is `postgres`/`mysql`/`mssql`/`oracle`/`snowflake`. `connectionString` is a `Value` — reach for `env(...)`, not a literal — stored as `context.connection_string_flex`. A bare string stores the older `context.connection_string` instead (an env-var name unless it looks like a URL); each form round-trips as itself.
|
|
831
841
|
- `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
|
|
832
842
|
- `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` — `items` is an array `Value`.
|
|
@@ -929,7 +939,7 @@ Microservices (the `microservice()` def and the statement that calls it):
|
|
|
929
939
|
- `c.array(a: Json[]) => Value` — Array constant (JSON string) → tag "const:array". Plain JSON literals only — a nested tagged value is rejected, same as c.obj.
|
|
930
940
|
- `c.expression(source: string) => Value` — Xano Expression Engine source, passed through VERBATIM → tag "const:expr2". The string IS the expression: c.expression('"Hi, " ~ $input.name'), c.expression("$var.price * $var.qty"). ⚠️ NOT VALIDATED — never parsed or type-checked, invisible to InferResponse, and untouched by a rename that updates every typed ref(); a typo surfaces at runtime or as a wrong answer. Use it ONLY for syntax the typed surfaces cannot express (~ concatenation, inline arithmetic, conditionals) — prefer ref/inp/col, withFilters+fl.*, and obj() (which BUILDS a checked expression). Not the expr() condition builder.
|
|
931
941
|
- `c.now() => Value` — Current time as epoch-ms — the engine-native const:epochms constant (no filter). Valid inline as a where/cmp operand. For cutoff math (cutoff = now - max_age) either compare inline or, for reuse/readability, hoist it into an s.set_var and compare against the var.
|
|
932
|
-
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. Still rejected: a filter ARGUMENT
|
|
942
|
+
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — 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.
|
|
933
943
|
- `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.
|
|
934
944
|
- `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it — an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).
|
|
935
945
|
- `col(name: string) => Value` — Reference a table column → tag "col".
|
|
@@ -1101,12 +1111,13 @@ database with a single `s.db.direct_query` UPDATE (`SET clicks = clicks + 1 WHER
|
|
|
1101
1111
|
which the DB applies atomically. Reserve the pipeline form for low-contention counters
|
|
1102
1112
|
where a rare lost update is acceptable.
|
|
1103
1113
|
⚠ `direct_query` needs the table's PHYSICAL Postgres name, which the typed surface
|
|
1104
|
-
does NOT expose: the engine derives
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
the
|
|
1109
|
-
|
|
1114
|
+
does NOT expose: the engine derives it from ids assigned at import — not knowable from a
|
|
1115
|
+
`table()` def (identity is a name + guid, not the numeric id) — and `sql_name` persists
|
|
1116
|
+
empty. The derived name is also NOT STABLE: a deploy is a full replace, so every table is
|
|
1117
|
+
created afresh and the id in its name moves each time, on the same unchanged project. So
|
|
1118
|
+
the safe counter drops out of the typed surface: resolve the physical name from
|
|
1119
|
+
`information_schema` inside the request that uses it, and never store, cache or hardcode
|
|
1120
|
+
one. A typed atomic path needs an engine change.
|
|
1110
1121
|
|
|
1111
1122
|
- `fl.add(value: decimal): decimal`
|
|
1112
1123
|
- `fl.append(value: <T>, path: text): <T>[]`
|
|
@@ -1198,11 +1209,11 @@ engine change.
|
|
|
1198
1209
|
- `fl.prepend(value: <T>, path: text): <T>[]`
|
|
1199
1210
|
- `fl.range(start: int, stop: int): int[]`
|
|
1200
1211
|
- `fl.reduce(initial_value: int, code: text, timeout?: int): any[]` — `code` is a JS body run per element; the ACCUMULATOR is `$result` (there is no `$acc`) and `initial_value` is REQUIRED — omitting it would slot the code as the initial value
|
|
1201
|
-
- `fl.regex_match(subject: text): text[]`
|
|
1202
|
-
- `fl.regex_match_all(subject: text): text[]`
|
|
1212
|
+
- `fl.regex_match(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
|
|
1213
|
+
- `fl.regex_match_all(subject: text): text[]` — piped value is the PATTERN, the arg is the subject — see `regex_test`
|
|
1203
1214
|
- `fl.regex_quote(delimiter?: text): text`
|
|
1204
|
-
- `fl.regex_replace(replacement: text, subject: text): text`
|
|
1205
|
-
- `fl.regex_test(subject: text): bool`
|
|
1215
|
+
- `fl.regex_replace(replacement: text, subject: text): text` — piped value is the PATTERN, `subject` is the text searched — see `regex_test`. The replacement comes FIRST
|
|
1216
|
+
- `fl.regex_test(subject: text): bool` — piped value is the PATTERN (build it with `c.regex(...)`); the arg is the subject — the REVERSE of `contains`/`starts_with`. Swapped, it answers false for every input with no error, so write `withFilters(c.regex("^a+$"), fl.regex_test(inp("s")))` (or name the arg: `fl.regex_test({ subject: inp("s") })`). A pattern found in the subject slot is refused at build time
|
|
1206
1217
|
- `fl.round(precision?: int): decimal`
|
|
1207
1218
|
- `fl.rtrim(mask?: text): text`
|
|
1208
1219
|
- `fl.secureid_decode(salt: text): int`
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.12
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -16,25 +16,26 @@ caller, `c.*` a constant — resolved at request time; JS operators over them do
|
|
|
16
16
|
compute (see Gotchas). Requests share no memory — state persists in tables or redis.
|
|
17
17
|
|
|
18
18
|
Coverage: object kinds 25/31, statement surfaces 214/214, filters 226 (226 typed).
|
|
19
|
-
Not authorable here: branch, market_item, realtime_channel, run.job, run.service, tablemap — these cannot be authored and do not survive a pull;
|
|
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
22
|
every gotcha, and control flow. Per-surface detail lives in the topic files listed
|
|
23
|
-
below — open the one whose condition matches the task,
|
|
24
|
-
exhaustive per-entry detail in NEITHER — a statement's
|
|
25
|
-
defaults, a filter's
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
below — open the one whose condition matches the task, skip the rest. For
|
|
24
|
+
exhaustive per-entry detail in NEITHER — a statement's field schema with engine
|
|
25
|
+
defaults, a filter's full argument list, the `storedName` mapping — do a TARGETED
|
|
26
|
+
lookup in the shipped `manifest.json` (a program imports it as
|
|
27
|
+
`@xanots/sdk/manifest.json`, needing `with { type: "json" }` in Node ESM;
|
|
28
|
+
it is ~60k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
|
|
28
29
|
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
29
30
|
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
31
|
+
⚠ `fields` is null on the 65 `declarative: false` statements (`db.get`, …): typed wrappers whose arguments are the factory's `.d.ts` signature. Null ≠ missing.
|
|
30
32
|
Select a statement on `sPath` (the `s.*` path you write), NOT `surface` (the XanoScript term): 24 of 214 differ — `var`→`set_var`, `break`→`foreach_break`, `foreach.remove`→`foreach_remove`, and every `expect.*`.
|
|
31
33
|
|
|
32
34
|
## Topic files
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`llms-full.txt` is everything concatenated — one fetch, for a reader that cannot open files.
|
|
36
|
+
Paths are relative to this file (`node_modules/@xanots/sdk/` once installed), so a
|
|
37
|
+
plain file read resolves them at the version you have.
|
|
38
|
+
`llms-full.txt` is everything concatenated — one fetch for a reader that cannot open files.
|
|
38
39
|
|
|
39
40
|
- [Object kinds](llms/object-kinds.md): Read when deciding WHAT to build — every authorable primitive (tables, endpoints, functions, tasks, agents, MCP, realtime, microservices, …), what each is, and which factory + register method build it. Read it too when a `register*` call will not typecheck because the array was built by `.flatMap()`/`.concat()` across modules.
|
|
40
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.
|
|
@@ -266,6 +267,10 @@ Non-obvious authoring rules:
|
|
|
266
267
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
268
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
269
|
check to the client.
|
|
270
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
271
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
272
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
273
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
269
274
|
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
275
|
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
276
|
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
@@ -320,18 +325,19 @@ Non-obvious authoring rules:
|
|
|
320
325
|
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
321
326
|
either way; only `deploy` ships them. The `node:fs` writers
|
|
322
327
|
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
323
|
-
NOT the browser-safe `@xanots/sdk` entry (
|
|
324
|
-
|
|
328
|
+
NOT the browser-safe `@xanots/sdk` entry (a frontend imports defs from it for
|
|
329
|
+
`getPath()`/`InferInput`).
|
|
325
330
|
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
326
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`.
|
|
327
334
|
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
328
335
|
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
329
336
|
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
330
337
|
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
331
|
-
⚠ A FLOOR — **~
|
|
338
|
+
⚠ A FLOOR — **~267 kB minified (~65 kB gzipped)** for the FIRST def; splitting modules
|
|
332
339
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
|
-
one, adds ~
|
|
334
|
-
def does will not reduce it.
|
|
340
|
+
one, adds ~2 kB — so reducing what a def does will not reduce it.
|
|
335
341
|
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
342
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
343
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
@@ -385,17 +391,18 @@ Non-obvious authoring rules:
|
|
|
385
391
|
return.
|
|
386
392
|
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
387
393
|
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
388
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped
|
|
389
|
-
`c.text("
|
|
390
|
-
input, so a precondition on it silently rejects all values
|
|
391
|
-
`c.regex(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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.
|
|
399
406
|
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
400
407
|
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
401
408
|
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xanots",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.",
|
|
5
5
|
"coverage": {
|
|
6
6
|
"objectKinds": {
|
|
@@ -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\"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref(\"row.id\") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref(\"row.id\") } }`, auto-wrapped through this) derives the same. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. Still rejected: a filter ARGUMENT
|
|
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\"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref(\"row.id\") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref(\"row.id\") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — 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",
|
|
@@ -9393,6 +9393,10 @@
|
|
|
9393
9393
|
"flag": "--json",
|
|
9394
9394
|
"description": "Force JSON on stdout (default: whenever stdout is not a terminal)"
|
|
9395
9395
|
},
|
|
9396
|
+
{
|
|
9397
|
+
"flag": "--no-refresh",
|
|
9398
|
+
"description": "Do not refresh the managed block in CLAUDE.md / AGENTS.md / the Cursor rule"
|
|
9399
|
+
},
|
|
9396
9400
|
{
|
|
9397
9401
|
"flag": "--help, -h",
|
|
9398
9402
|
"description": "Show help for the command and exit"
|