@xanots/sdk 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +38 -0
- package/dist/.build-fingerprint +1 -1
- package/dist/bin.js +7 -6
- package/dist/bundle.d.ts +218 -0
- package/dist/bundle.js +143 -0
- package/dist/{chunk-5YCQ2QHH.js → chunk-22TKBSDV.js} +2 -2
- package/dist/{chunk-YYFXVYPX.js → chunk-4IF54NU5.js} +3 -3
- package/dist/{chunk-W24FJHPD.js → chunk-4Q7ZOHH7.js} +2 -2
- package/dist/{chunk-4BXJGVZ3.js → chunk-BC2C5GVI.js} +2 -69
- package/dist/chunk-BSK7ELHU.js +70 -0
- package/dist/chunk-EHP3WPEG.js +21 -0
- package/dist/{chunk-75Z74TA7.js → chunk-G4EJMQLD.js} +2 -2
- package/dist/{chunk-I7DQDJAM.js → chunk-HYBN4H3F.js} +38 -46
- package/dist/chunk-OHX6MIUZ.js +184 -0
- package/dist/{chunk-VKSOTZK3.js → chunk-OWGCOGKK.js} +155 -3
- package/dist/{chunk-LBYWGMOA.js → chunk-QK7ZQJLP.js} +140 -20
- package/dist/{chunk-5R73LFWK.js → chunk-QYMAZRAU.js} +7 -7
- package/dist/{chunk-ACCBOMCB.js → chunk-RCT7UX7B.js} +12 -12
- package/dist/{chunk-P6TAVLOX.js → chunk-UOZMSF4C.js} +15 -8
- package/dist/{chunk-7JDT4PBU.js → chunk-VAF6A3YD.js} +5 -179
- package/dist/{chunk-OYMR5AMJ.js → chunk-XEOX6AM7.js} +2 -2
- package/dist/cli.js +6 -5
- package/dist/{codegen-command-GB3H2KQ7.js → codegen-command-FUT2KJB6.js} +15 -12
- package/dist/codegen.d.ts +2 -1
- package/dist/{deploy-command-QTIVA22A.js → deploy-command-IP7V7GT4.js} +8 -7
- package/dist/index.d.ts +6 -4
- package/dist/index.js +10 -9
- package/dist/{init-command-MAXULNAD.js → init-command-NPVL32L6.js} +8 -7
- package/dist/internal.d.ts +4 -4
- package/dist/internal.js +62 -40
- package/dist/io-P2H75UV2.js +12 -0
- package/dist/{lock-GFXD6G2E.js → lock-46FWYE4D.js} +3 -2
- package/dist/{lock-commands-DQ7CUMIV.js → lock-commands-ZZKZ4LZJ.js} +8 -7
- package/dist/node.d.ts +5 -4
- package/dist/node.js +12 -11
- package/dist/{preflight-command-GZ2GE5RN.js → preflight-command-K346GPTY.js} +8 -7
- package/dist/{release-command-YUBTNHVX.js → release-command-IMNTIVWK.js} +14 -13
- package/dist/response-BQVQ24l1.d.ts +844 -0
- package/dist/{store-BG1UPZ3Z.d.ts → store-BLyNeQ8S.d.ts} +16 -3
- package/dist/{upgrade-command-FB5QJ363.js → upgrade-command-BN3EHAOI.js} +8 -7
- package/dist/{workspace-command-43P42FBP.js → workspace-command-MNK7Y7MQ.js} +15 -12
- package/dist/{response-CVAE2kMj.d.ts → xdo-BjJj5W_E.d.ts} +1 -837
- package/guides/typed-frontend.md +1 -1
- package/llms/filters.md +11 -10
- package/llms/kinds-core.md +1 -1
- package/llms/statements-data.md +3 -1
- package/llms/values.md +1 -1
- package/llms-full.txt +41 -30
- package/llms.txt +25 -17
- package/manifest.json +2 -2
- package/package.json +7 -2
- package/dist/io-M7XZEMK7.js +0 -11
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`.
|
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.
|
|
@@ -54,6 +54,8 @@ primary key `id`):
|
|
|
54
54
|
- `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
|
|
55
55
|
- `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
|
|
56
56
|
- `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.
|
|
57
|
+
- 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.
|
|
58
|
+
- ⚠ 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
59
|
- `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
60
|
- `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
|
|
59
61
|
- `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 carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
15
|
+
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
16
16
|
- `ref(name: string, opts?: { safe?: boolean }) => Value` — Reference a stack variable → tag "var". Pass { safe: true } for null-safe nested access — a dotted ref("owner.user_id", { safe: true }) compiles through the get filter so it resolves to null instead of raising "Unable to locate var" when the base is null.
|
|
17
17
|
- `inp(name: string) => Value` — Reference a function/endpoint input → tag "input". Resolves ONLY against the `input` block of the def it sits in — a value produced earlier in the stack is `ref("var.field")`, not `inp("field")`. A name that is not declared here deploys clean and fails at runtime with ERROR_FATAL "Unable to locate input: <name>" on every branch that reads it; `export()` warns, and `--strict` fails the build. Sending the name in the request does NOT rescue it — an undeclared input is never bound, so the call fails identically with the value present. A dotted path drills INTO a declared input (`inp("action.amount")` needs a declared `action`).
|
|
18
18
|
- `col(name: string) => Value` — Reference a table column → tag "col".
|
package/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# xanots v0.0.
|
|
1
|
+
# xanots v0.0.11
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -23,7 +23,9 @@ every gotcha, and control flow. Per-surface detail lives in the topic files list
|
|
|
23
23
|
below — open the one whose condition matches the task, and skip the rest. For
|
|
24
24
|
exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
|
|
25
25
|
defaults, a filter's complete argument list, the engine `storedName` mapping — do a
|
|
26
|
-
TARGETED lookup in the shipped `manifest.json` (
|
|
26
|
+
TARGETED lookup in the shipped `manifest.json` (a program imports it as
|
|
27
|
+
`@xanots/sdk/manifest.json`, which Node ESM needs `with { type: "json" }` on;
|
|
28
|
+
grep or `jq` the one entry you need;
|
|
27
29
|
it is ~65k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
|
|
28
30
|
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
29
31
|
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
@@ -266,6 +268,10 @@ Non-obvious authoring rules:
|
|
|
266
268
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
269
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
270
|
check to the client.
|
|
271
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
272
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
273
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
274
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
269
275
|
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
276
|
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
277
|
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
@@ -320,18 +326,19 @@ Non-obvious authoring rules:
|
|
|
320
326
|
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
321
327
|
either way; only `deploy` ships them. The `node:fs` writers
|
|
322
328
|
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
323
|
-
NOT the browser-safe `@xanots/sdk` entry (
|
|
324
|
-
|
|
329
|
+
NOT the browser-safe `@xanots/sdk` entry (a frontend imports defs from it for
|
|
330
|
+
`getPath()`/`InferInput`).
|
|
325
331
|
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
326
332
|
the lock model) is on `@xanots/sdk/internal` and is never needed to author.
|
|
333
|
+
READING a bundle back is `@xanots/sdk/bundle` — a statement walker (`2.if.0` paths),
|
|
334
|
+
a structural hash, `mvp:*` catalog, `tableRefOf`.
|
|
327
335
|
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
328
336
|
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
329
337
|
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
330
338
|
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
331
339
|
⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
|
|
332
340
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
|
-
one, adds ~1 kB
|
|
334
|
-
def does will not reduce it.
|
|
341
|
+
one, adds ~1 kB — so reducing what a def does will not reduce it.
|
|
335
342
|
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
343
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
344
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
@@ -385,17 +392,18 @@ Non-obvious authoring rules:
|
|
|
385
392
|
return.
|
|
386
393
|
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
387
394
|
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
388
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped
|
|
389
|
-
`c.text("
|
|
390
|
-
input, so a precondition on it silently rejects all values
|
|
391
|
-
`c.regex(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped, and the
|
|
396
|
+
ARGUMENT is the subject. A bare `c.text("^…$")` is an invalid pattern that matches
|
|
397
|
+
*nothing* for every input, so a precondition on it silently rejects all values.
|
|
398
|
+
`c.regex(body, "i")` wraps + escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
|
|
399
|
+
Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
|
|
400
|
+
answers false for EVERY input, so an `if (matches) reject` guard admits what it
|
|
401
|
+
refuses. `withFilters` throws on a bare `c.text` pattern from ANY position in
|
|
402
|
+
the chain (a normalizer in front of the regex filter is refused too; nothing upstream
|
|
403
|
+
adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match`
|
|
404
|
+
is the same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
|
|
405
|
+
`export()` warns on a reversed pair in stored bytes. Better still:
|
|
406
|
+
a native typed input (`input.email`) over hand-rolled validation.
|
|
399
407
|
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
400
408
|
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
401
409
|
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
|
@@ -522,7 +530,7 @@ to survive a rename).
|
|
|
522
530
|
- `verb`: `"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD"` (required), UPPERCASE. Anything else — most often a lowercase `"post"` — makes `query()` THROW, because Xano does NOT reject it: it stores the verb as NULL, a null verb serves as GET, and the endpoint then answers on the wrong method while the one you meant 404s `Unable to locate request.`
|
|
523
531
|
- `apiGroup`: an `apiGroup()` def handle (or its name) — binds by guid, stable across syncs. Raw numeric `apiGroupId?` is the escape hatch and wins if both given.
|
|
524
532
|
- `auth`: `false` (no auth) or an auth-table id; `responseType`: `"standard" | "stream"` (default `standard`) — any other spelling THROWS, since Xano stores an unrecognized one as NULL and a null buffers as `standard`, so a misspelled stream quietly does not stream.
|
|
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.
|
|
533
|
+
- `name` is the endpoint PATH within the group. A `{param}` segment is a URL PATH PARAM bound to the input of the same name, and segments chain: `name: "blog/{slug}/review/{review_id}"` + `input: { slug: input.text(), review_id: input.int() }`. Read it with `inp("slug")` like any other input. Every `{param}` MUST have a matching input or `query()` THROWS — Xano treats an unbound marker as inert route text, so the endpoint would answer on the path and see nothing. A `{param}` need NOT be a whole segment (`"blog/post-{slug}"` routes fine), but its type must fit one segment (no object/list/json/file/geo/vector); there are no wildcards or patterns. `required: true` is NOT demanded (the engine's editor leaves path inputs unmarked). The CONVERSE is warned, not enforced: an input a `GET`/`DELETE`/`HEAD` looks ONE ROW up by (`s.db.get`/`get_by_id`/`has`/by-field edit/patch/delete) belongs in the path — `export()` warns `query.path-segment-candidate`. It still serves `?blog_id=1`, but the route is not addressable and `getPath()` types STATIC, so a caller cannot pass the value positionally. A segment is any value naming WHICH resource is wanted, not just an id (`"shop/{country}"`). An input that NARROWS A LIST (`s.db.query`) stays a query-string param. Inputs absent from the path are ordinary query-string/body params. Name charset is ONLY `A-Za-z0-9_-/{}`, max 200: a `.` (`"export.zip"`) is NOT rejected by Xano — it stores an EMPTY name that deploys clean then 404s forever, so `query()` THROWS. Use `"export_zip"` and set the extension in the response headers.
|
|
526
534
|
- **Client recipe:** `q.getPath({ params: { slug: "hello" } })` → `/api:<canonical>/blog/hello` — never interpolate by hand. `getPath` percent-encodes each value (so `?`/`#`/spaces stay in their segment) and throws on what encoding cannot contain: a `/`, and a value that IS `.`/`..` (a URL parser drops those before routing — `%2e` counts — addressing a different endpoint). The keys are typed from the literal `name`, so a typo is a compile error. The HANDLE's `q.toSearchParams(input)` drops path params for a GET; the free `query.toSearchParams(input)` has no view of the route and keeps every key.
|
|
527
535
|
- `apiGroup({ name, guid?, canonical?, description?, docs?, swagger?, apiGroupEnabled?, documentation?, cors? })` — a query container; register it and bind queries to it via their `apiGroup`.
|
|
528
536
|
- `cors?`: `{ mode?, allowOrigins?: string[], allowHeaders?: string[], allowCredentials?, maxAge?, allowMethods?: { get?, post?, put?, patch?, delete?, head? } }`.
|
|
@@ -785,7 +793,7 @@ primary key `id`):
|
|
|
785
793
|
|
|
786
794
|
- `s.db.get({ table, fieldName?, fieldValue, lock?, output?, as? })` — one row by field match; `output` restricts returned columns (and overrides column visibility — it can pull `internal` columns like a password hash).
|
|
787
795
|
- `s.db.get_by_id({ table, id, output?, addon?, tableAlias?, as? })` — get by primary key. Takes `id`, NOT `fieldName`/`fieldValue`; binds the row or `null` for an id that names no row. Both spellings are live in pulled workspaces.
|
|
788
|
-
- ⚠ `id` is validated `>= 1`, so the `0` sentinel an optional `f.tableRef` stores fails the request with HTTP 400 `Value is less than the minimum value of 1` — it does NOT bind `null`. The throw is not scoped to the lookup: inside a `foreach` it kills the whole request, so one unset FK loses every other row's work. Read a nullable FK with the field-match form, which binds `null` on `0` and lets the loop finish: `s.db.get({ table, fieldName: "id", fieldValue: ref("row.fk"), as })`.
|
|
796
|
+
- ⚠ `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
797
|
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
790
798
|
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
791
799
|
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
@@ -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 carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
942
|
+
- `obj(fields: Record<string, Value | nested>) => ObjValue<typeof fields>` — Dynamic object value → tag "const:expr2" (an object-literal expression string). The dynamic sibling of c.obj: members may be inp/ref/auth/col values, env()/setting()/sys.*, c.now(), c.* constants, nested records, or arrays — and each member may carry a FILTER CHAIN (withFilters + fl.*), which renders as the expression pipe `$var.row|get:"a.b"`. That matters most for the null-safe drill: db.get binds null on a miss, so ref(path, { safe: true }) inside an obj() is the normal shape, not a workaround — you do NOT need a preceding s.set_var to hoist it. The member record rides the return type, so InferResponse resolves each member the way it resolves a top-level response key — `response: { user: obj({ id: ref("row.id") }) }` derives `{ user: { id: Col | null } }`, and a raw nested object literal (`response: { user: { id: ref("row.id") } }`, auto-wrapped through this) derives the same. A constant record or list has two spellings that both work and render identically: bare (`{ a: 1 }`, `[]`) or `c.obj(...)`/`c.array([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args.
|
|
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.11
|
|
2
2
|
|
|
3
3
|
> TypeScript SDK that compiles a typed Xano workspace into the importable packageExport JSON bundle.
|
|
4
4
|
|
|
@@ -23,7 +23,9 @@ every gotcha, and control flow. Per-surface detail lives in the topic files list
|
|
|
23
23
|
below — open the one whose condition matches the task, and skip the rest. For
|
|
24
24
|
exhaustive per-entry detail in NEITHER — a statement's full field schema with engine
|
|
25
25
|
defaults, a filter's complete argument list, the engine `storedName` mapping — do a
|
|
26
|
-
TARGETED lookup in the shipped `manifest.json` (
|
|
26
|
+
TARGETED lookup in the shipped `manifest.json` (a program imports it as
|
|
27
|
+
`@xanots/sdk/manifest.json`, which Node ESM needs `with { type: "json" }` on;
|
|
28
|
+
grep or `jq` the one entry you need;
|
|
27
29
|
it is ~65k tokens, so never read it whole). Its top-level keys are `name`, `version`, `description`, `coverage`, `values`, `objectKinds`, `fieldTypes`, `statements`, `filters`, `cli`, `cliGlobalFlags`. `statements` and `filters` are ARRAYS, not maps — SELECT, do not index:
|
|
28
30
|
jq '.statements[] | select(.sPath=="db.get")' manifest.json
|
|
29
31
|
jq '.filters[] | select(.name=="json_decode")' manifest.json
|
|
@@ -266,6 +268,10 @@ Non-obvious authoring rules:
|
|
|
266
268
|
check-in — use `db.query({ where: [expr(col("habit"), "=", ...), expr(col("date"), "=", ...)], as })`
|
|
267
269
|
(a `where` array is ANDed) and branch on the result, rather than pushing the
|
|
268
270
|
check to the client.
|
|
271
|
+
- **A column named `run` is reserved.** The table deploys and reads back fine, then
|
|
272
|
+
EVERY `s.db.add` into it 400s — at any column type, with or without a value — and
|
|
273
|
+
the error names the column while complaining about the VALUE. Use `run_id`. Exact,
|
|
274
|
+
case-sensitive, one name: `Run`/`runs`/`run_id` are fine. `--strict` fails on it.
|
|
269
275
|
- **System columns are auto-injected.** `id` + `created_at` are prepended to
|
|
270
276
|
every table (`system: true` by default); declaring them by hand is redundant.
|
|
271
277
|
`id` is an `int` PK by default; pass `idType: "uuid"` on the table for a uuid key.
|
|
@@ -320,18 +326,19 @@ Non-obvious authoring rules:
|
|
|
320
326
|
`xanots export`/`deploy` CLI path. Seed rows are never emitted into the bundle
|
|
321
327
|
either way; only `deploy` ships them. The `node:fs` writers
|
|
322
328
|
(`writeBundle`/`writeArtifact`) and lock-file I/O import from `@xanots/sdk/node`,
|
|
323
|
-
NOT the browser-safe `@xanots/sdk` entry (
|
|
324
|
-
|
|
329
|
+
NOT the browser-safe `@xanots/sdk` entry (a frontend imports defs from it for
|
|
330
|
+
`getPath()`/`InferInput`).
|
|
325
331
|
The compiler machinery (per-kind `encode*`, the registries, the bundle serializer,
|
|
326
332
|
the lock model) is on `@xanots/sdk/internal` and is never needed to author.
|
|
333
|
+
READING a bundle back is `@xanots/sdk/bundle` — a statement walker (`2.if.0` paths),
|
|
334
|
+
a structural hash, `mvp:*` catalog, `tableRefOf`.
|
|
327
335
|
- **Client bundle size / tree-shaking.** `@xanots/sdk` is `sideEffects: false` and pulls
|
|
328
336
|
no Node built-ins, so a bundler drops unused SDK exports. But importing a **def** for its
|
|
329
337
|
`getPath()`/`verb`/`getUrl()`/`getChannel()` also pulls whatever its `stack` references:
|
|
330
338
|
the `s.*`/`c.*` factory CALLS run at module load to BUILD it. Types are free.
|
|
331
339
|
⚠ A FLOOR — **~289 kB minified (~57 kB gzipped)** for the FIRST def; splitting modules
|
|
332
340
|
never removes it. The floor is the RUNTIME, not the def: a second def, or a much richer
|
|
333
|
-
one, adds ~1 kB
|
|
334
|
-
def does will not reduce it.
|
|
341
|
+
one, adds ~1 kB — so reducing what a def does will not reduce it.
|
|
335
342
|
Fix: `xanots routes <entry> --emit xano/routes.gen.ts` (`paths` is an accepted alias) — verbs, paths, and sockets as
|
|
336
343
|
plain data importing NOTHING, still compile-checked: `routePath("blog/{slug}", { slug })`,
|
|
337
344
|
`channelPath("rooms/{room_id}", { room_id })`, `socketUrl("chat", baseUrl)` (tenant base
|
|
@@ -385,17 +392,18 @@ Non-obvious authoring rules:
|
|
|
385
392
|
return.
|
|
386
393
|
- **Build regex-filter patterns with `c.regex(body, flags?)`, never `c.text`.**
|
|
387
394
|
The regex filters (`regex_test`/`regex_match`/`regex_replace`/…) are pattern-piped
|
|
388
|
-
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped
|
|
389
|
-
`c.text("
|
|
390
|
-
input, so a precondition on it silently rejects all values
|
|
391
|
-
`c.regex(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
PHP `preg_*`: the piped value is the PATTERN and must be delimiter-wrapped, and the
|
|
396
|
+
ARGUMENT is the subject. A bare `c.text("^…$")` is an invalid pattern that matches
|
|
397
|
+
*nothing* for every input, so a precondition on it silently rejects all values.
|
|
398
|
+
`c.regex(body, "i")` wraps + escapes it for you (a JS `RegExp` too: `c.regex(/^…$/i)`).
|
|
399
|
+
Reversed — subject piped, pattern in the argument — reads correctly, type-checks, and
|
|
400
|
+
answers false for EVERY input, so an `if (matches) reject` guard admits what it
|
|
401
|
+
refuses. `withFilters` throws on a bare `c.text` pattern from ANY position in
|
|
402
|
+
the chain (a normalizer in front of the regex filter is refused too; nothing upstream
|
|
403
|
+
adds the delimiters) and on a pattern found in the subject slot; `s.expect.to_match`
|
|
404
|
+
is the same PATTERN slot, refused both ways; a `ref`/`inp` pattern is passed through untouched.
|
|
405
|
+
`export()` warns on a reversed pair in stored bytes. Better still:
|
|
406
|
+
a native typed input (`input.email`) over hand-rolled validation.
|
|
399
407
|
- **Compose a rule set as SIBLINGS, not a folded chain.** `and(...rules)` takes any
|
|
400
408
|
number of terms and encodes flat; `rules.reduce((acc, r) => and(acc, r))` nests one
|
|
401
409
|
container per rule, which costs quadratic bytes (512 terms: 394 KiB flat, 21 MiB
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xanots",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
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 carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args."
|
|
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([...])` — reach for the bare form, it is shorter. NEST WITH A RAW RECORD, not an inner obj() call: an inner call yields a const:expr2 value, which the expression serializer has no spelling for and THROWS. The legacy blank `c.obj(null)` is refused here (it evaluates to null, not {}) — write c.null() or c.obj(). Still rejected: a filter ARGUMENT carrying its own chain (a trailing | binds to the whole value, not one argument), a DISABLED filter (an expression string cannot record that), and the output/response/toolset/reg tags — build those in a prior step and ref() them. Use for e.g. s.ai.agent.run args."
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"name": "ref",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xanots/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
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",
|
|
@@ -52,7 +52,12 @@
|
|
|
52
52
|
"types": "./dist/internal.d.ts",
|
|
53
53
|
"import": "./dist/internal.js"
|
|
54
54
|
},
|
|
55
|
-
"./
|
|
55
|
+
"./bundle": {
|
|
56
|
+
"types": "./dist/bundle.d.ts",
|
|
57
|
+
"import": "./dist/bundle.js"
|
|
58
|
+
},
|
|
59
|
+
"./package.json": "./package.json",
|
|
60
|
+
"./manifest.json": "./manifest.json"
|
|
56
61
|
},
|
|
57
62
|
"files": [
|
|
58
63
|
"dist",
|
package/dist/io-M7XZEMK7.js
DELETED