@xanots/sdk 0.0.1
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/LICENSE +21 -0
- package/README.md +384 -0
- package/dist/agent-file-refresh-7XCACU34.js +95 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +184 -0
- package/dist/capture-HUV5BNTC.js +32 -0
- package/dist/chunk-26CNBZE2.js +963 -0
- package/dist/chunk-33ND6LKO.js +78 -0
- package/dist/chunk-3DUAGYGP.js +1550 -0
- package/dist/chunk-3IGNIP6R.js +89 -0
- package/dist/chunk-47WDWMBJ.js +14 -0
- package/dist/chunk-4BXJGVZ3.js +318 -0
- package/dist/chunk-4HT3BNZ7.js +675 -0
- package/dist/chunk-4YMD2OOZ.js +23 -0
- package/dist/chunk-5WGEURVI.js +156 -0
- package/dist/chunk-5YTBTF25.js +29 -0
- package/dist/chunk-6DHBYBTO.js +166 -0
- package/dist/chunk-76QBEIGO.js +66 -0
- package/dist/chunk-7DKX2SPN.js +100 -0
- package/dist/chunk-7REDODS2.js +468 -0
- package/dist/chunk-C56BC2FY.js +254 -0
- package/dist/chunk-DGSF2Q5H.js +60 -0
- package/dist/chunk-EMCAWMHC.js +3982 -0
- package/dist/chunk-EZG76F7R.js +159 -0
- package/dist/chunk-FJ6ZS5KB.js +3607 -0
- package/dist/chunk-GNPVYOPB.js +17 -0
- package/dist/chunk-HJPTWBLH.js +38 -0
- package/dist/chunk-IZW6MK3U.js +248 -0
- package/dist/chunk-LH6Z6KFV.js +247 -0
- package/dist/chunk-MZUI3JNX.js +93 -0
- package/dist/chunk-NDZFBZHC.js +167 -0
- package/dist/chunk-NOO35BQ2.js +37 -0
- package/dist/chunk-NUQCEOKA.js +13 -0
- package/dist/chunk-QTNO2WD6.js +56 -0
- package/dist/chunk-RVB7ITKN.js +49 -0
- package/dist/chunk-TRU5OHQ5.js +6904 -0
- package/dist/chunk-VKFQJEMW.js +814 -0
- package/dist/chunk-VTIL47DT.js +125 -0
- package/dist/chunk-WHOJWOSV.js +112 -0
- package/dist/chunk-WKP4D5MO.js +1692 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-XQ22GLYS.js +118 -0
- package/dist/chunk-YDZYMZBE.js +670 -0
- package/dist/chunk-YGPIY3WT.js +1146 -0
- package/dist/chunk-YYRDLFSJ.js +602 -0
- package/dist/chunk-ZUTSMMAG.js +46 -0
- package/dist/cli.d.ts +331 -0
- package/dist/cli.js +33 -0
- package/dist/codegen-command-OOKK6AZK.js +40 -0
- package/dist/codegen.d.ts +114 -0
- package/dist/codegen.js +71 -0
- package/dist/completion-TIPPZN7H.js +298 -0
- package/dist/config-NL33PN4D.js +84 -0
- package/dist/deploy-command-4EQ7XX5G.js +451 -0
- package/dist/env-target-IFJ5QDHC.js +16 -0
- package/dist/ephemeral-command-3X3I4ULC.js +242 -0
- package/dist/index.d.ts +4477 -0
- package/dist/index.js +300 -0
- package/dist/init-command-R6H7ZXW5.js +27 -0
- package/dist/internal.d.ts +74 -0
- package/dist/internal.js +2065 -0
- package/dist/io-AMIKRLPC.js +11 -0
- package/dist/live-diff-Y3AKETHE.js +196 -0
- package/dist/lock-3CVKALKT.js +37 -0
- package/dist/lock-commands-XTR2YKLY.js +232 -0
- package/dist/login-command-ACJF6KWQ.js +167 -0
- package/dist/logout-command-MX3MJS5U.js +68 -0
- package/dist/loop-OM3P2Z6O.js +93 -0
- package/dist/marketplace-command-RATXTKXN.js +379 -0
- package/dist/meta-client-OW5WKWW7.js +170 -0
- package/dist/node.d.ts +59 -0
- package/dist/node.js +442 -0
- package/dist/profile-command-SWJ3SPKR.js +77 -0
- package/dist/release-command-VV5UVZKL.js +427 -0
- package/dist/response-CVAE2kMj.d.ts +1915 -0
- package/dist/routes-manifest-SP3ZXLMR.js +244 -0
- package/dist/runtime-V4C3AC3A.js +13 -0
- package/dist/sandbox-details-command-HJE5SPVG.js +18 -0
- package/dist/sandbox-export-command-QCJY4GMV.js +24 -0
- package/dist/static-host-D6KS7X45.js +138 -0
- package/dist/store-BQ97nKIF.d.ts +7760 -0
- package/dist/test-command-TSPBQHBM.js +383 -0
- package/dist/validate-command-4ERHJSCO.js +122 -0
- package/dist/verify-rollout-FAVQ6EJY.js +40 -0
- package/dist/workspace-K72NP7SX.js +49 -0
- package/dist/workspace-command-OF23DPNI.js +156 -0
- package/dist/workspace-export-AJMGN3CQ.js +10 -0
- package/llms/fields.md +104 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +11 -0
- package/llms/kinds-core.md +68 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +111 -0
- package/llms/legacy.md +43 -0
- package/llms/object-kinds.md +31 -0
- package/llms/statements-calls.md +23 -0
- package/llms/statements-catalog.md +309 -0
- package/llms/statements-data.md +58 -0
- package/llms/statements-runtime.md +48 -0
- package/llms/tests.md +58 -0
- package/llms/triggers.md +22 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +1672 -0
- package/llms.txt +483 -0
- package/manifest.json +9592 -0
- package/package.json +110 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Statement catalog
|
|
2
|
+
|
|
3
|
+
> Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace. Look here after the control-flow core in the router does not cover what you need.
|
|
4
|
+
|
|
5
|
+
### (top-level)
|
|
6
|
+
|
|
7
|
+
- `s.await({ as?: string; ids?: value; timeout?: value })`
|
|
8
|
+
- `s.foreach_break` [special]
|
|
9
|
+
- `s.comment` [special]
|
|
10
|
+
- `s.conditional` [special]
|
|
11
|
+
- `s.foreach_continue` [special]
|
|
12
|
+
- `s.for` [special]
|
|
13
|
+
- `s.foreach_remove` [special]
|
|
14
|
+
- `s.foreach` [special]
|
|
15
|
+
- `s.group` [special]
|
|
16
|
+
- `s.lambda({ as?: string; code?: value; timeout?: value })`
|
|
17
|
+
- `s.precondition({ expr?: comparison; error_type?: "standard" | "notfound" | "toomanyrequests" | "accessdenied" | "unauthorized" | "badrequest" | "inputerror"; error?: value; payload?: value })`
|
|
18
|
+
- `s.return` [special]
|
|
19
|
+
- `s.switch` [special]
|
|
20
|
+
- `s.throw({ name?: string; value: value })`
|
|
21
|
+
- `s.try_catch` [special]
|
|
22
|
+
- `s.update_var` [special]
|
|
23
|
+
- `s.set_var` [special]
|
|
24
|
+
- `s.while` [special]
|
|
25
|
+
|
|
26
|
+
### action
|
|
27
|
+
|
|
28
|
+
- `s.action.call` [special]
|
|
29
|
+
- `s.action.package.call` [special]
|
|
30
|
+
|
|
31
|
+
### addon
|
|
32
|
+
|
|
33
|
+
- `s.addon.call` [special]
|
|
34
|
+
|
|
35
|
+
### ai
|
|
36
|
+
|
|
37
|
+
- `s.ai.agent.run` [special]
|
|
38
|
+
- `s.ai.external.mcp.server_details({ as?: string; url?: value; bearer_token?: value; connection_type?: "sse" | "stream" })`
|
|
39
|
+
- `s.ai.external.mcp.tool.list({ as?: string; url?: value; bearer_token?: value; connection_type?: "sse" | "stream" })`
|
|
40
|
+
- `s.ai.external.mcp.tool.run({ as?: string; url?: value; bearer_token?: value; connection_type?: "sse" | "stream"; tool?: value; args?: value })`
|
|
41
|
+
|
|
42
|
+
### api
|
|
43
|
+
|
|
44
|
+
- `s.api.call` [special]
|
|
45
|
+
- `s.api.request` [special, output]
|
|
46
|
+
- `s.api.stream({ value: value })`
|
|
47
|
+
|
|
48
|
+
### array
|
|
49
|
+
|
|
50
|
+
- `s.array.difference({ as?: string; expr?: value; value?: value; by?: value })`
|
|
51
|
+
- `s.array.every({ expr?: value; as?: string; if?: comparison })` [output] → as: boolean
|
|
52
|
+
- `s.array.filter_count({ expr?: value; as?: string; if?: comparison })`
|
|
53
|
+
- `s.array.filter({ expr?: value; as?: string; if?: comparison })`
|
|
54
|
+
- `s.array.find_index({ expr?: value; as?: string; if?: comparison })`
|
|
55
|
+
- `s.array.find({ expr?: value; as?: string; if?: comparison })` [output]
|
|
56
|
+
- `s.array.group_by({ as?: string; expr?: value; by?: value })`
|
|
57
|
+
- `s.array.has({ expr?: value; as?: string; if?: comparison })`
|
|
58
|
+
- `s.array.intersection({ as?: string; expr?: value; value?: value; by?: value })`
|
|
59
|
+
- `s.array.map` [special]
|
|
60
|
+
- `s.array.merge({ name?: string; value?: value })` [output]
|
|
61
|
+
- `s.array.partition({ expr?: value; as?: string; if?: comparison })`
|
|
62
|
+
- `s.array.pop({ name?: string; as?: string })` [output]
|
|
63
|
+
- `s.array.push({ name?: string; value: value })` [output]
|
|
64
|
+
- `s.array.shift({ name?: string; as?: string })`
|
|
65
|
+
- `s.array.union` [special]
|
|
66
|
+
- `s.array.unshift({ name?: string; value: value })`
|
|
67
|
+
|
|
68
|
+
### cloud
|
|
69
|
+
|
|
70
|
+
- `s.cloud.algolia.request({ as?: string; application_id: value; api_key: value; url: value; method?: "POST" | "GET" | "DELETE" | "PUT"; payload: value })` [output]
|
|
71
|
+
- `s.cloud.aws.opensearch.document({ as?: string; auth_type?: "IAM" | "master"; key_id?: value; access_key?: value; region?: value; base_url: value; method?: "GET" | "POST" | "PUT" | "DELETE"; index?: value; doc_id?: value; doc?: value })`
|
|
72
|
+
- `s.cloud.aws.opensearch.query({ as?: string; auth_type?: "IAM" | "master"; key_id?: value; access_key?: value; region?: value; base_url?: value; index?: value; payload?: value; size?: value; from?: value; included_fields?: value; return_type?: "search" | "count"; expression?: value; sort?: value })`
|
|
73
|
+
- `s.cloud.aws.opensearch.request({ as?: string; auth_type?: "IAM" | "master"; key_id?: value; access_key?: value; region?: value; method?: "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH"; url?: value; query?: value })`
|
|
74
|
+
- `s.cloud.aws.s3.delete_file({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value })`
|
|
75
|
+
- `s.cloud.aws.s3.get_file_info({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value })`
|
|
76
|
+
- `s.cloud.aws.s3.list_directory({ as?: string; bucket: value; region: value; key: value; secret: value; prefix?: value; next_page_token?: value })`
|
|
77
|
+
- `s.cloud.aws.s3.read_file({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value })`
|
|
78
|
+
- `s.cloud.aws.s3.sign_url({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value; ttl?: value })`
|
|
79
|
+
- `s.cloud.aws.s3.upload_file({ as?: string; bucket: value; region: value; key: value; secret: value; file_key?: value; file: value; metadata?: value; object_lock_mode?: "compliance" | "governance"; object_lock_retain_until?: value })`
|
|
80
|
+
- `s.cloud.azure.storage.delete_file({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value })`
|
|
81
|
+
- `s.cloud.azure.storage.get_file_info({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value })`
|
|
82
|
+
- `s.cloud.azure.storage.list_directory({ as?: string; account_name: value; account_key: value; container_name: value; path?: value })`
|
|
83
|
+
- `s.cloud.azure.storage.read_file({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value })`
|
|
84
|
+
- `s.cloud.azure.storage.sign_url({ as?: string; account_name: value; account_key: value; container_name: value; path: value; ttl?: value })`
|
|
85
|
+
- `s.cloud.azure.storage.upload_file({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value; file: value; metadata?: value })`
|
|
86
|
+
- `s.cloud.elasticsearch.document({ as?: string; auth_type?: "Basic" | "Bearer" | "API Key"; key_id: value; access_key: value; base_url: value; index: value; method?: "GET" | "POST" | "PUT" | "DELETE"; doc_id: value; doc: value })`
|
|
87
|
+
- `s.cloud.elasticsearch.query({ as?: string; auth_type?: "Basic" | "Bearer" | "API Key"; key_id?: value; access_key?: value; base_url?: value; index?: value; payload?: value; size?: value; from?: value; included_fields?: value; return_type?: "search" | "count"; expression?: value; sort?: value })`
|
|
88
|
+
- `s.cloud.elasticsearch.request({ as?: string; auth_type?: "Basic" | "Bearer" | "API Key"; key_id?: value; access_key?: value; method?: "POST" | "GET" | "PUT" | "DELETE" | "PATCH"; url?: value; payload?: value })`
|
|
89
|
+
- `s.cloud.google.storage.delete_file({ as?: string; service_account: value; bucket: value; filePath: value })`
|
|
90
|
+
- `s.cloud.google.storage.get_file_info({ as?: string; service_account: value; bucket: value; filePath: value })`
|
|
91
|
+
- `s.cloud.google.storage.list_directory({ as?: string; service_account: value; bucket: value; path: value })`
|
|
92
|
+
- `s.cloud.google.storage.read_file({ as?: string; service_account: value; bucket: value; filePath: value })`
|
|
93
|
+
- `s.cloud.google.storage.sign_url({ as?: string; service_account: value; bucket: value; filePath: value; method?: "GET" | "POST"; ttl?: value })`
|
|
94
|
+
- `s.cloud.google.storage.upload_file({ as?: string; service_account: value; bucket: value; filePath: value; file: value; metadata?: value })`
|
|
95
|
+
- `s.cloud.job.await` [special]
|
|
96
|
+
- `s.cloud.job.status` [special]
|
|
97
|
+
- `s.cloud.job` [special]
|
|
98
|
+
|
|
99
|
+
### datadog
|
|
100
|
+
|
|
101
|
+
- `s.datadog.log_bulk({ entries: value; connection?: value })`
|
|
102
|
+
- `s.datadog.log({ message?: value; status?: "debug" | "info" | "notice" | "warn" | "error" | "critical" | "alert" | "emergency"; attributes?: value; service?: value; source?: value; env?: value; hostname?: value; tags?: value; timestamp?: value; connection?: value })`
|
|
103
|
+
- `s.datadog.metric_bulk({ entries: value; connection?: value })`
|
|
104
|
+
- `s.datadog.metric({ metric?: value; value: value; type?: "count" | "gauge" | "rate" | "histogram" | "distribution"; tags?: value; service?: value; source?: value; env?: value; hostname?: value; timestamp?: value; connection?: value })`
|
|
105
|
+
|
|
106
|
+
### db
|
|
107
|
+
|
|
108
|
+
- `s.db.add_or_edit` [special] → as: InferRow<T> (upserts and never misses)
|
|
109
|
+
- `s.db.add` [special] → as: InferRow<T> (the full inserted row incl. id/created_at)
|
|
110
|
+
- `s.db.bulk.add` [special]
|
|
111
|
+
- `s.db.bulk.delete` [special] → as: number (count of deleted rows)
|
|
112
|
+
- `s.db.bulk.patch` [special] → as: InferRow<T>[]
|
|
113
|
+
- `s.db.bulk.update` [special]
|
|
114
|
+
- `s.db.del` [special] → as: null (the engine deletes and returns no value; throws NotFound on a miss)
|
|
115
|
+
- `s.db.direct_query` [special]
|
|
116
|
+
- `s.db.edit` [special] → as: InferRow<T> (the full post-mutation row; throws NotFound on a miss)
|
|
117
|
+
- `s.db.external.mssql.direct_query` [special]
|
|
118
|
+
- `s.db.external.mysql.direct_query` [special]
|
|
119
|
+
- `s.db.external.oracle.direct_query` [special]
|
|
120
|
+
- `s.db.external.postgres.direct_query` [special]
|
|
121
|
+
- `s.db.external.snowflake.direct_query` [special]
|
|
122
|
+
- `s.db.get_by_id` [special]
|
|
123
|
+
- `s.db.get` [special] → as: InferRow<T> | null (binds null on a miss, never throws)
|
|
124
|
+
- `s.db.has` [special] → as: boolean
|
|
125
|
+
- `s.db.patch` [special] → as: InferRow<T> (the full post-mutation row; throws NotFound on a miss)
|
|
126
|
+
- `s.db.query` [special] → as: InferRow<T>[] (a paging envelope when metadata paging is on)
|
|
127
|
+
- `s.db.schema` [special]
|
|
128
|
+
- `s.db.set_datasource({ value: value; workspace_id?: value })`
|
|
129
|
+
- `s.db.transaction` [special]
|
|
130
|
+
- `s.db.truncate` [special]
|
|
131
|
+
|
|
132
|
+
### debug
|
|
133
|
+
|
|
134
|
+
Despite the namespace, `s.debug.stop` is a REAL control-flow terminator, not a development aid: it ends the request with an error value (`mvp:die`). It is also exported flat as `die()` — the one flat alias whose name does not mirror its `s.*` path, so there is no `s.die`. Compare `s.throw`, which raises a named error the surrounding `s.try_catch` can catch.
|
|
135
|
+
|
|
136
|
+
- `s.debug.log({ value: value })`
|
|
137
|
+
- `s.debug.stop({ value: value })`
|
|
138
|
+
|
|
139
|
+
### expect
|
|
140
|
+
|
|
141
|
+
Assertions. **Put them in a `workflowTest({...})` stack** — assert on what a `.call` bound with `as`. That is where they belong and effectively the only place to author them. They are NOT inert elsewhere, which is the part worth knowing: a failure raises and aborts whatever stack it is in, so an `s.expect.*` left in a `query`/`function`/`task` takes the request down with an HTTP 500 carrying the assertion's own message (`to_equal failed - expected value 2 does not equal 1`). Treat one outside a `workflowTest` as a mistake to remove, not as a check that quietly does nothing. Two behaviours to know when writing them: `to_throw` sees only an error carrying a MESSAGE, so a failure whose message is empty — an endpoint answering `ERROR_CODE_ACCESS_DENIED` is the common one — reports "response is ok" as though nothing failed, and `to_be_within` EXCLUDES both bounds — `min < expr < max` — while `s.security.random_number`'s bounds are inclusive.
|
|
142
|
+
|
|
143
|
+
- `s.expect.to_be_defined({ expr?: value })`
|
|
144
|
+
- `s.expect.to_be_empty({ expr?: value })`
|
|
145
|
+
- `s.expect.to_be_false({ expr?: value })`
|
|
146
|
+
- `s.expect.to_be_greater_than({ expr?: value; value?: value })`
|
|
147
|
+
- `s.expect.to_be_in_the_future({ expr?: value })`
|
|
148
|
+
- `s.expect.to_be_in_the_past({ expr?: value })`
|
|
149
|
+
- `s.expect.to_be_less_than({ expr?: value; value?: value })`
|
|
150
|
+
- `s.expect.to_be_null({ expr?: value })`
|
|
151
|
+
- `s.expect.to_be_true({ expr?: value })`
|
|
152
|
+
- `s.expect.to_be_within({ expr?: value; min?: value; max?: value })`
|
|
153
|
+
- `s.expect.to_contain({ expr?: value; value?: value })`
|
|
154
|
+
- `s.expect.to_end_with({ expr?: value; value?: value })`
|
|
155
|
+
- `s.expect.to_equal({ expr?: value; value?: value })`
|
|
156
|
+
- `s.expect.to_match({ expr?: value; value?: value })`
|
|
157
|
+
- `s.expect.to_not_be_defined({ expr?: value })`
|
|
158
|
+
- `s.expect.to_not_be_null({ expr?: value })`
|
|
159
|
+
- `s.expect.to_not_equal({ expr?: value; value?: value })`
|
|
160
|
+
- `s.expect.to_start_with({ expr?: value; value?: value })`
|
|
161
|
+
- `s.expect.to_throw` [special]
|
|
162
|
+
|
|
163
|
+
### function
|
|
164
|
+
|
|
165
|
+
- `s.function.call` [special]
|
|
166
|
+
- `s.function.run` [special]
|
|
167
|
+
|
|
168
|
+
### math
|
|
169
|
+
|
|
170
|
+
- `s.math.add({ name?: string; value: value })` [output] → as: number
|
|
171
|
+
- `s.math.bitwise.and({ name?: string; value: value })` [output] → as: number
|
|
172
|
+
- `s.math.bitwise.or({ name?: string; value: value })` [output] → as: number
|
|
173
|
+
- `s.math.bitwise.xor({ name?: string; value: value })` [output] → as: number
|
|
174
|
+
- `s.math.div({ name?: string; value: value })`
|
|
175
|
+
- `s.math.mod({ name?: string; value: value })`
|
|
176
|
+
- `s.math.mul({ name?: string; value: value })`
|
|
177
|
+
- `s.math.sub({ name?: string; value: value })`
|
|
178
|
+
|
|
179
|
+
### microservice
|
|
180
|
+
|
|
181
|
+
- `s.microservice.request` [special]
|
|
182
|
+
|
|
183
|
+
### middleware
|
|
184
|
+
|
|
185
|
+
- `s.middleware.call` [special]
|
|
186
|
+
|
|
187
|
+
### object
|
|
188
|
+
|
|
189
|
+
- `s.object.entries({ as?: string; value?: value })` [output] → as: [string, unknown][]
|
|
190
|
+
- `s.object.keys({ as?: string; value?: value })` [output] → as: string[]
|
|
191
|
+
- `s.object.values({ as?: string; value?: value })` [output] → as: unknown[]
|
|
192
|
+
|
|
193
|
+
### realtime
|
|
194
|
+
|
|
195
|
+
- `s.realtime.get_session({ as?: string })`
|
|
196
|
+
- `s.realtime.publish` [special]
|
|
197
|
+
|
|
198
|
+
### redis
|
|
199
|
+
|
|
200
|
+
- `s.redis.count({ as?: string; key: value })`
|
|
201
|
+
- `s.redis.decr({ as?: string; key: value; by?: value })`
|
|
202
|
+
- `s.redis.del({ key: value })`
|
|
203
|
+
- `s.redis.get({ as?: string; key: value })`
|
|
204
|
+
- `s.redis.has({ as?: string; key: value })`
|
|
205
|
+
- `s.redis.incr({ as?: string; key: value; by?: value })`
|
|
206
|
+
- `s.redis.keys({ as?: string; search: value })`
|
|
207
|
+
- `s.redis.pop({ as?: string; key: value; count?: value })`
|
|
208
|
+
- `s.redis.push({ as?: string; key: value; value: value })`
|
|
209
|
+
- `s.redis.range({ as?: string; key: value; start?: value; stop?: value })`
|
|
210
|
+
- `s.redis.ratelimit({ as?: string; key: value; max?: value; ttl?: value; error?: value })`
|
|
211
|
+
- `s.redis.remove({ as?: string; key: value; value: value; count?: value })`
|
|
212
|
+
- `s.redis.set({ as?: string; key: value; data: value; ttl?: value; create_only?: value })`
|
|
213
|
+
- `s.redis.shift({ as?: string; key: value; count?: value })`
|
|
214
|
+
- `s.redis.unshift({ as?: string; key: value; value: value })`
|
|
215
|
+
|
|
216
|
+
### security
|
|
217
|
+
|
|
218
|
+
- `s.security.check_password({ as?: string; text_password?: value; hash_password?: value })` [output] → as: boolean (true when the plaintext matches the stored hash. ⚠ input.password double-hashes — pass input.text() plaintext)
|
|
219
|
+
- `s.security.create_auth_token` [special]
|
|
220
|
+
- `s.security.create_curve_key({ as?: string; curve?: "P-256" | "P-384" | "P-521"; format?: "object" | "base64" })`
|
|
221
|
+
- `s.security.create_guid` [special]
|
|
222
|
+
- `s.security.create_password({ as?: string; character_count?: value; require_lowercase?: value; require_uppercase?: value; require_digit?: value; require_symbol?: value; symbol_whitelist?: value })`
|
|
223
|
+
- `s.security.create_rsa_key({ as?: string; bits?: value; format?: "object" | "base64" })`
|
|
224
|
+
- `s.security.create_secret_key({ as?: string; bits?: value; format?: "object" | "base64" })`
|
|
225
|
+
- `s.security.create_uuid({ as?: string })`
|
|
226
|
+
- `s.security.decrypt({ as?: string; data?: value; algorithm?: "aes-128-cbc" | "aes-192-cbc" | "aes-256-cbc" | "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; key?: value; iv?: value })`
|
|
227
|
+
- `s.security.encrypt({ as?: string; data?: value; algorithm?: "aes-128-cbc" | "aes-192-cbc" | "aes-256-cbc" | "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; key?: value; iv?: value })`
|
|
228
|
+
- `s.security.jwe_decode({ as?: string; token?: value; key?: value; check_claims?: value; key_algorithm?: "A128KW" | "A192KW" | "A256KW" | "A128GCMKW" | "A192GCMKW" | "A256GCMKW" | "ECDH-ES+A128KW" | "ECDH-ES+A192KW" | "ECDH-ES+A256KW"; content_algorithm?: "A128GCM" | "A192GCM" | "A256GCM" | "A128CBC-HS256" | "A192CBC-HS384" | "A256CBC-HS512"; timeDrift?: value })`
|
|
229
|
+
- `s.security.jwe_encode({ as?: string; headers?: value; claims?: value; key?: value; key_algorithm?: "A128KW" | "A192KW" | "A256KW" | "A128GCMKW" | "A192GCMKW" | "A256GCMKW" | "ECDH-ES+A128KW" | "ECDH-ES+A192KW" | "ECDH-ES+A256KW"; content_algorithm?: "A128GCM" | "A192GCM" | "A256GCM" | "A128CBC-HS256" | "A192CBC-HS384" | "A256CBC-HS512"; ttl?: value })`
|
|
230
|
+
- `s.security.jws_decode({ as?: string; token?: value; key?: value; check_claims?: value; signature_algorithm?: "PS256" | "PS384" | "PS512" | "RS256" | "RS384" | "RS512" | "HS256" | "HS384" | "HS512" | "ES256" | "ES384" | "ES512"; timeDrift?: value })`
|
|
231
|
+
- `s.security.jws_encode({ as?: string; headers?: value; claims?: value; key?: value; signature_algorithm?: "PS256" | "PS384" | "PS512" | "RS256" | "RS384" | "RS512" | "HS256" | "HS384" | "HS512" | "ES256" | "ES384" | "ES512"; ttl?: value })`
|
|
232
|
+
- `s.security.random_bytes({ as?: string; length?: value })`
|
|
233
|
+
- `s.security.random_number({ as?: string; min?: value; max?: value })`
|
|
234
|
+
|
|
235
|
+
### storage
|
|
236
|
+
|
|
237
|
+
- `s.storage.create_attachment({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value; include_meta?: boolean; type?: value })`
|
|
238
|
+
- `s.storage.create_audio({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value })` [output]
|
|
239
|
+
- `s.storage.create_file_resource({ as?: string; filename: value; filedata: value })` [output]
|
|
240
|
+
- `s.storage.create_image({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value })` [output]
|
|
241
|
+
- `s.storage.create_video({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value })` [output]
|
|
242
|
+
- `s.storage.delete_file({ pathname: value })` [output]
|
|
243
|
+
- `s.storage.read_file_resource({ as?: string; value: value })`
|
|
244
|
+
- `s.storage.sign_private_url({ as?: string; pathname: value; ttl?: value })`
|
|
245
|
+
|
|
246
|
+
### stream
|
|
247
|
+
|
|
248
|
+
- `s.stream.from_csv({ as?: string; value: value; separator?: value; enclosure?: value; escape_char?: value })` [output]
|
|
249
|
+
- `s.stream.from_jsonl({ as?: string; value: value })`
|
|
250
|
+
- `s.stream.from_request` [special]
|
|
251
|
+
|
|
252
|
+
### task
|
|
253
|
+
|
|
254
|
+
- `s.task.call` [special]
|
|
255
|
+
|
|
256
|
+
### text
|
|
257
|
+
|
|
258
|
+
- `s.text.append({ name?: string; value: value })`
|
|
259
|
+
- `s.text.contains({ name?: string; as?: string; value: value })`
|
|
260
|
+
- `s.text.ends_with({ name?: string; as?: string; value: value })`
|
|
261
|
+
- `s.text.icontains({ name?: string; as?: string; value: value })`
|
|
262
|
+
- `s.text.iends_with({ name?: string; as?: string; value: value })`
|
|
263
|
+
- `s.text.istarts_with({ name?: string; as?: string; value: value })`
|
|
264
|
+
- `s.text.ltrim({ name?: string; value: value })`
|
|
265
|
+
- `s.text.prepend({ name?: string; value: value })`
|
|
266
|
+
- `s.text.rtrim({ name?: string; value: value })`
|
|
267
|
+
- `s.text.starts_with({ name?: string; as?: string; value: value })`
|
|
268
|
+
- `s.text.trim({ name?: string; value: value })`
|
|
269
|
+
|
|
270
|
+
### tool
|
|
271
|
+
|
|
272
|
+
- `s.tool.call` [special]
|
|
273
|
+
|
|
274
|
+
### trigger
|
|
275
|
+
|
|
276
|
+
- `s.trigger.call` [special]
|
|
277
|
+
|
|
278
|
+
### util
|
|
279
|
+
|
|
280
|
+
- `s.util.geo_distance({ as?: string; latitude_1?: value; longitude_1?: value; latitude_2?: value; longitude_2?: value })` → as: number (great-circle distance in METRES (a decimal) — divide by 1000 for km. Identical points return 0)
|
|
281
|
+
- `s.util.get_all_input({ as?: string })`
|
|
282
|
+
- `s.util.get_env({ as?: string })`
|
|
283
|
+
- `s.util.get_input` [special]
|
|
284
|
+
- `s.util.get_raw_input` [special]
|
|
285
|
+
- `s.util.get_vars({ as?: string })`
|
|
286
|
+
- `s.util.ip_lookup({ as?: string; value: value })` → as: IpLookupResult | null (NESTED, not flat: { continent: {code,name}, country: {code,name}, region: {code,name}, city: {name}, postal: {code}, location: {latitude, longitude, tz, radius} }. Coordinates are ref("geo.location.latitude"/".longitude"), place names ref("geo.city.name"/"geo.region.name"/"geo.country.name"); radius is KILOMETRES. ⚠ Every leaf is nullable and region/city/postal commonly ARE null for a routable public address — that is a normal hit, not a failed lookup. `city` is an OBJECT, so a bare ref("geo.city") into a text column fails on the object and { safe: true } does NOT help; drill to city.name with a fallback. The whole var is null for an unresolvable address)
|
|
287
|
+
- `s.util.post_process` [special]
|
|
288
|
+
- `s.util.send_email({ as?: string; service_provider?: "resend" | "xano"; api_key?: value; subject?: value; message?: value; to?: value; bcc?: value; cc?: value; from?: value; reply_to?: value; scheduled_at?: value })`
|
|
289
|
+
- `s.util.set_header({ value: value; duplicates?: "replace" | "append" })`
|
|
290
|
+
- `s.util.sleep({ value: value })`
|
|
291
|
+
- `s.util.template_engine({ as?: string; value: value })`
|
|
292
|
+
|
|
293
|
+
### webflow
|
|
294
|
+
|
|
295
|
+
- `s.webflow.request` [special]
|
|
296
|
+
|
|
297
|
+
### workflow_test
|
|
298
|
+
|
|
299
|
+
Run another workflow test from inside one. Pass the `workflowTest()` def handle, not a name.
|
|
300
|
+
|
|
301
|
+
- `s.workflow_test.call` [special]
|
|
302
|
+
|
|
303
|
+
### zip
|
|
304
|
+
|
|
305
|
+
- `s.zip.add_to_archive({ file: value; filename: value; zip: value; password?: value; password_encryption?: value })`
|
|
306
|
+
- `s.zip.create_archive({ as?: string; filename: value; password?: value; password_encryption?: "standard" | "AES-128" | "AES-192" | "AES-256" })`
|
|
307
|
+
- `s.zip.delete_from_archive({ filename: value; zip: value; password?: value })`
|
|
308
|
+
- `s.zip.extract({ as?: string; zip: value; password?: value })`
|
|
309
|
+
- `s.zip.view_contents({ as?: string; zip: value; password?: value })`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Array and database statements
|
|
2
|
+
|
|
3
|
+
> Read when the stack reads or writes rows (`s.db.*`), or transforms an array in place (`s.array.map`, `s.array.union`).
|
|
4
|
+
|
|
5
|
+
Array blocks (an `if`/`transform` is applied per item):
|
|
6
|
+
|
|
7
|
+
- `s.array.map({ source, as?, transform? })` — `transform` is either a per-item `Value` expression (each item maps to that value) or a **record of values** (each item maps to an object with those keys), or a list of `{ key, value }` pairs, for a key computed per item or two rows sharing one. Use `ref("$this")` for the item and `ref("$index")` for its position. These are THIS statement's own bindings, in a value expression — not the JavaScript lambda contract (see **Lambda bodies**), which binds a different set per surface and is written with `lam.fn`.
|
|
8
|
+
- `s.array.union({ source, with?, as?, transform? })` — set-union two arrays.
|
|
9
|
+
|
|
10
|
+
DB reads/writes (`table` is a def handle or name; `fieldName` defaults to the
|
|
11
|
+
primary key `id`):
|
|
12
|
+
|
|
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
|
+
- `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` like `db.get`. Both spellings are live in pulled workspaces.
|
|
15
|
+
- `s.db.has({ table, fieldName?, fieldValue, as? })` — existence test.
|
|
16
|
+
- `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
|
|
17
|
+
- `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
|
|
18
|
+
- A row CELL takes a tagged `Value`, a nested object of sub-keys, or a bare JS literal typed against that column: `row: { is_hidden: true, notes: "…" }` encodes exactly as `{ is_hidden: c.bool(true), notes: c.text("…") }`. The tag comes from the COLUMN, not the literal — `10` on an `f.decimal()` column is `const:decimal`, not `const:int` — so a literal contradicting its column is a compile error on a `f.*`-schema table (`{ is_hidden: "yes" }` on an `f.bool()` column) and throws at encode on a raw-`ColumnDef[]` one. An `f.enum()` column keeps its member union. A column with no literal form — obj/json/list/geo/vector/file — still needs `c.obj`/`c.array`.
|
|
19
|
+
- `null` is accepted on EVERY column, including ones that refuse every other literal, and encodes `const:null` — a write OF null, not the same as omitting the key (omitted takes the column's type default on `add`: `[]` for a list, `{}` for obj/json, else `null`; on `edit` it is left untouched). A column's `nullable` is not consulted; the engine refuses a null it forbids.
|
|
20
|
+
- An `f.password()` cell takes the PLAINTEXT — the column hashes on write, so a pre-hashed value, or a hashing filter on the cell, stores a hash of a hash that `security.check_password` can never match.
|
|
21
|
+
- `s.db.edit({ table, fieldName?, fieldValue, row?, data?, output?, as? })` — update by field match.
|
|
22
|
+
- `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial (`data` is an object value).
|
|
23
|
+
On these three, `output` restricts the columns of the RETURNED row only — it does not change
|
|
24
|
+
what is written. Not offered on `db.del`/`db.has` (their result is a scalar) or on
|
|
25
|
+
`db.add_or_edit` (no output envelope).
|
|
26
|
+
- `s.db.add_or_edit({ table, fieldName?, fieldValue, row?, data?, as? })` — upsert.
|
|
27
|
+
- `s.db.query({ table, where?, additionalWhere?, bind?, sort?, paging?, external?, returnType?, distinct?, eval?, output?, lock?, addon?, as? })` — search.
|
|
28
|
+
- `where` / `additionalWhere` — `expr(...)`, an `expr[]` (ANDed), or a raw `Value`. Rides `context.search`.
|
|
29
|
+
- ⚠ `ignoreEmpty` DROPS the predicate when the operand is empty — it does not match zero rows. On an `in` comparison an empty list therefore returns the UNFILTERED set, so never use it to scope rows to a permitted-id list: an empty list of permissions returns everything.
|
|
30
|
+
- For the full operator set use `cmp(left, op, right, { ignoreEmpty? })` — `op`: `in`/`not in`/`like`/`ilike`/`between`/`contains`/`includes`/`overlaps`/`@>`/`~`/`search`/… plus the `expr` comparisons. Database-only — a runtime condition takes the `expr` set only.
|
|
31
|
+
- Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
|
|
32
|
+
- 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.
|
|
33
|
+
- ⚠ 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.
|
|
34
|
+
- `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.
|
|
35
|
+
- ⚠ 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.
|
|
36
|
+
- `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`
|
|
37
|
+
- `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") }`.
|
|
38
|
+
- `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.
|
|
39
|
+
- 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. Exported as `QUERY_EXPRESSION_FILTERS`/`VECTOR_FILTERS`.
|
|
40
|
+
- **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.
|
|
41
|
+
- `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`.
|
|
42
|
+
- ⚠ 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.
|
|
43
|
+
- The alias it qualifies WITH is `tableAlias` when you set one, otherwise the table's name — and the statement DECLARES that alias (`dbo.as`) so the qualified name resolves. Nothing to do by hand; a bare `name` is the form to write.
|
|
44
|
+
- `sort: [{ sortBy: <col>, dir?: "asc"|"desc"|"rand" }]` and `paging: { page?, per_page?, offset?, totals?, metadata?, search?, sort? }` ride `context.return.list`.
|
|
45
|
+
- ⚠ `paging` with a page/per_page/offset field and `metadata` on (the DEFAULT) wraps the result in an envelope `{ items: Row[], curPage, nextPage, prevPage, offset, perPage, itemsReceived }` — plus `itemsTotal`/`pageTotal` when `totals: true` — instead of a bare `Row[]`. `InferResponse` reflects it. Pass `metadata: false` to keep the bare array.
|
|
46
|
+
- Read `nextPage` (`number|null`) as the typed has-next signal.
|
|
47
|
+
- **Input-bound paging:** `page`/`per_page`/`offset` also accept a `Value` (`inp("page")`), riding `context.simpleExternal` while the static block stays the engine gate (`enabled:true`). `paging.search`/`sort` are `Value` dynamic overrides.
|
|
48
|
+
- A `search`/`sort`-only `paging` (no numeric field) does NOT paginate.
|
|
49
|
+
- `external: { value, permissions? }` — the classic whole-config blob (forces the gate on). It falls back to input-bound `paging` when it resolves empty, so supplying both is valid.
|
|
50
|
+
- `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
|
|
51
|
+
- `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
|
|
52
|
+
- `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.
|
|
53
|
+
- `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.
|
|
54
|
+
- `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
|
|
55
|
+
- `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` — `items` is an array `Value`.
|
|
56
|
+
- ⚠ `bulk.add` **drops `id` on every row unless `allowIdField: true`** (silently, next sequence value instead) — the opposite of `seed`, where `id` pins. Rows referenced by a foreign key need `allowIdField: true`; literal `items` carrying `id` without it throw. `bulk.update`/`patch` keep `id` (their match key).
|
|
57
|
+
- ⚠ **`bulk.update` is a whole-row REPLACE: every column an item OMITS is zeroed** (`""`/`0`/`null`), HTTP 200, no error — `{ id: 7, status: "done" }` blanks the rest of row 7. **Use `s.db.bulk.patch`** for the partial write "update these rows" means. `export()` warns on a STATIC `items` missing columns (`--strict` fails); a `ref`/`inp` `items` is uninspectable.
|
|
58
|
+
- `s.db.bulk.delete({ table, where?, allRows?, as? })` — deletes rows by a `context.search` filter. `where` is the same surface as `s.db.query` (`expr(...)`/`cmp(...)`, `and(...)`/`or(...)` groups, an array of those ANDed, or a raw `Value`) and encodes through the identical `{expression:[…]}` search shape. ⚠ A filter that constrains nothing deletes **every** row, so a missing or empty `where` **throws**: pass the filter, or `allRows: true` for a deliberate wipe (both together also throw). `allRows` emits the empty search the engine requires and returns the deleted count; reach for `s.db.truncate({ table, reset: true })` when the id sequence should restart too.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Statement runtime behavior
|
|
2
|
+
|
|
3
|
+
> Read when you need to know what a statement's `as:` output actually holds, or why a bound variable is not the shape you expected.
|
|
4
|
+
|
|
5
|
+
Runtime behavior (what the `as:` output holds, and misses):
|
|
6
|
+
|
|
7
|
+
- `db.get` binds **`null`** when no row matches (it does NOT throw) — so the output is `InferRow<typeof table> | null`; null-check it. On a hit it binds the **full row**. (`db.has` is the boolean existence test.)
|
|
8
|
+
- `db.edit` binds the **full, post-mutation row** (the freshly-written values, not the pre-edit ones). `db.add` binds the **full inserted row**, including the auto-assigned `id` and `created_at`. So `InferRow<typeof table>` is the right response type for those two. `db.del` **binds `null`** — the engine deletes the row and returns no value, so don't return the `as` var expecting the deleted row.
|
|
9
|
+
- Unlike `db.get`, `db.edit` and `db.del` **throw** `NotFound` (HTTP 404) when no row matches the field. `db.add` throws on a unique-constraint violation.
|
|
10
|
+
- **`InferResponse<typeof query>`** derives an endpoint's response type (read-side round trip, no codegen). It resolves object-literal responses to those keys; a `response: ref("x")` returning a variable bound by a TOP-LEVEL db op on a `table()` resolves to that op's result:
|
|
11
|
+
| statement | resolves to | on a miss |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `db.add` / `db.edit` / `db.patch` / `db.add_or_edit` | `Row` (the full written row, non-nullable) | throws — `NotFound`/404 for `edit`/`patch`, a unique-constraint error for `add`; `add_or_edit` upserts and never misses |
|
|
14
|
+
| `db.get` | `Row \| null` | binds `null` rather than throwing |
|
|
15
|
+
| `db.query` / `db.bulk.patch` | `Row[]` | — |
|
|
16
|
+
| `db.has` | `boolean` | — |
|
|
17
|
+
| `db.bulk.delete` | `number` (count) | — |
|
|
18
|
+
| `db.del`, `db.bulk.add`/`bulk.update`, raw `direct_query` | `unknown` (the engine leaves them untyped) | — |
|
|
19
|
+
- A `get`/`query` `output: [...]` selection narrows to a `Pick` (still `| null` for `get`). A dotted `ref("row.col")` into a `db.get` row projects that column carrying the `| null` (→ `Col | null`).
|
|
20
|
+
- A value reshaped by a filter/lambda, or a variable built by control flow / `set_var`, also resolves to `unknown`.
|
|
21
|
+
- A CALL binds the TARGET's response type: `s.function.call`/`s.function.run`/`s.api.call`/`s.tool.call` given a def HANDLE brand their `as` with `InferResponse` of the target, so `ref("out.field")` types to that field. What the target resolves to is what propagates — declare `responseShape` on the TARGET to fix every caller at once. A target named by STRING has no def to read and stays `unknown`, as does an async `s.function.run` (it binds a job handle, not the result).
|
|
22
|
+
- ⚠ In an object-literal response the KEYS are always known, but a VALUE is typed only when it traces to a binding — a `ref`, or a `ref` under `withFilters`. A CONSTANT does not trace: `response: { success: c.bool(true), hello: c.text("hi") }` derives `{ success: unknown, hello: unknown }`, NOT `{ success: boolean, hello: string }`. Same for an `inp` outside a trace. Only a reference carries a type the derivation can follow. Declare `responseShape` when a client needs those keys typed.
|
|
23
|
+
- A NESTED member resolves to any depth by these same rules, in either spelling — `{ user: obj({ id: ref("row.id") }) }` and the raw literal `{ user: { id: ref("row.id") } }` both derive `{ user: { id: Col | null } }`. A raw scalar member types itself (`{ count: 3 }` → `number`).
|
|
24
|
+
- Runs on every response-bearing kind — `query`, `defineFunction`, `realtimeMessage`, `tool`, `middleware`, and the response-bearing triggers — each of which also accepts `responseShape`. A trigger builds its stack and response through CALLBACKS (`stack: (t) => [...]`), and the trace follows through them.
|
|
25
|
+
- Close any `unknown` by declaring `responseShape` on the def (`responseShape: null as InferRow<typeof t> | null`) — the declaration ALWAYS overrides derivation. It is available on EVERY kind listed above, `query` included.
|
|
26
|
+
- The shapes that never derive, and the one line that closes each: rows built in `s.lambda` (JS is opaque to the walk) → `responseShape: [] as Row[]`; a var assigned across `s.conditional`/`s.switch` branches → `responseShape` for the union you meant; a call whose target is named by STRING, or an async `s.function.run` → give the TARGET a `responseShape` (a handle then carries it) or declare it here. Reach for `responseShape` as the FIRST move on these, not after fighting the derivation.
|
|
27
|
+
- ⚠ A RUNTIME guard does not narrow the STATIC type. A `s.precondition` (or a conditional) proving a `db.get` row non-null leaves `InferResponse` at `Row | null`, because the walk reads the stack's shape and not its control flow — the `| null` is correct about the derivation and stale about your stack. Declare `responseShape` to drop it rather than asserting at every call site.
|
|
28
|
+
- ⚠ A `resultStrategy: "replace"` middleware attached `post` reshapes the endpoint's output at runtime, which the static walk cannot see. Declare `responseShape` when a post middleware rewrites the response.
|
|
29
|
+
- ⚠ **Spreading a `Statement[]` helper into a stack kills the whole walk.** The trace needs the stack's TUPLE type, so `...myHelper()` where the helper returns `Statement[]` widens it and EVERY `as` in that stack — including ones declared after the spread — stops resolving. The response then types as `StackTupleWidened`, whose name says so. Fix: return `statements(s.a(...), s.b(...))` from the helper (a const-generic identity export — the tuple survives the spread). A helper that builds its array in a LOOP cannot be a tuple; declare `responseShape` there.
|
|
30
|
+
- **Addons** enrich returned rows. `db.query`/`get`/`add`/`edit`/`patch` accept `addon: [{ addon, as, input?, output?, children? }]`; `db.add_or_edit`/`del`/`has`/`truncate` take no `addon`.
|
|
31
|
+
- `addon` is the target (name or def handle). `as` is the destination on the row — a bare alias (`"_user"`) or a dotted `offset.alias`, authored relative to a row. Under a metadata paging envelope the `items[]` offset is prefixed automatically; writing it yourself is tolerated and not double-prefixed.
|
|
32
|
+
- `input` maps addon inputs — bind a parent-row column with `out(col)`. `output` restricts addon columns. `children` nests addons.
|
|
33
|
+
- An addon is a single table-bound db query, NOT a statement stack: `addon({ name, table, tableAlias?, where?, sort?, output: [cols], cardinality?: "single"|"list"|"count"|"exists"|"aggregate", group?, eval?, input?, context? })`, registered via `registerAddons([...])`.
|
|
34
|
+
- `table` auto-fills the `context.dbo` binding. ⚠ Never author `table: null` — that is a BROKEN table-less addon returning nothing; `codegen` emits it only for an already-broken pulled object.
|
|
35
|
+
- `tableAlias` is its SQL alias (`context.dbo.as`), qualifying `where`/`sort` columns (`col("merchant.id")`).
|
|
36
|
+
- `where`/`sort` take the same surface as `s.db.query` and encode `context.search`/`context.sort`. `where` is the predicate binding the addon to the parent row — `expr(col("id"), "=", inp("user_id"))`.
|
|
37
|
+
- `cardinality` shapes the result (`context.return.type`, omitted for the `"list"` default). Rarer context (`eval`/`bind`/`lock`) stays raw `context` passthrough.
|
|
38
|
+
- Attaching a typed `addon({ table, output })` handle merges its alias (the last `as` segment) onto the row in `InferResponse`: `{cols}` for `single`, `{cols}[]` for `list`, `number` for `count`, `boolean` for `exists`, and for `aggregate` an array keyed by the `group`/`eval` aliases (`unknown` values; `unknown` when neither is declared).
|
|
39
|
+
- An attachment-level `output` narrows an object/array graft further. A bare-NAME reference grafts `unknown` — narrow it at the call site.
|
|
40
|
+
- ⚠ An alias that shadows an existing column on the queried table throws at build time; rename with a `_` prefix.
|
|
41
|
+
- **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack. Attach with the host's `middleware: { pre, post }` field on `query`/`function`/`task`/`tool`/`apiGroup` (NOT triggers): each phase is an ordered list of middleware refs (def handle or name), or `{ middleware, active: false }` to keep an entry disabled. Providing a phase **overrides** it (sets the stored `pre_customize`/`post_customize` flag); omitting a phase **inherits** the parent tier's chain — the engine resolves Query → API Group → Workspace at request time (override, not merge; the API-Group tier applies to queries — functions/tasks/tools have no API-group binding and inherit straight from the workspace). Prefer a def handle over a bare name when the middleware pins an explicit `guid`. `pre: middleware.clear()` (an empty list) overrides with nothing — stop inheriting. Workspace-level defaults are the terminal tier: `workspaceConfig({ middleware: { query: { pre }, function, task, tool } })` emits the flat `{host}_{phase}` map (no `_customize` flags) — setting it replaces the whole workspace map, so unlisted hosts are cleared; omit the field to leave existing workspace middleware untouched. Distinct from `s.middleware.call` (inline invoke).
|
|
42
|
+
- **Middleware request context.** A `pre` middleware runs **after** auth resolution, so `auth()` is available inside the middleware when the host is authenticated (its `auth` names an auth table); on a public host `auth()` is `null`. This matters for the canonical use — a rate limit keyed by `auth("id")`: on an authenticated endpoint the bucket is per-user, but attach the same middleware to a public endpoint and every anonymous caller keys under the same `null` id (one shared bucket), silently. To catch that, `export()` **warns** (never blocks) when a middleware whose stack references `auth()` is directly attached to a host where `auth()` may be null — a `query` with no auth table, a `task` (scheduled, never authenticated), or a `function`/`tool` (whose auth is caller-dependent). An authenticated query (its own `auth` table set) is skipped. The check is direct-attachment only; a middleware reaching a public query via API-group/workspace tier inheritance is not caught.
|
|
43
|
+
- **Rate-limit recipe (the canonical middleware).** Per-user rate limiting is the most common middleware. Author it with `s.redis.ratelimit` and a **composite key** built via the filter chain — `"prefix" + auth("id")` does not exist, you build the key: `middleware({ name: "write_rl", exceptionPolicy: "rethrow", stack: [ s.redis.ratelimit({ key: withFilters(c.text("rl:write:"), fl.concat(auth("id"))), max: c.int(10), ttl: c.int(30), error: c.text("Too fast.") }) ] })`. `exceptionPolicy` defaults to `"rethrow"`, which is what makes a tripped limit abort with HTTP 429; `"silent"` would let the over-limit request through. Attach it with `middleware: { pre: [writeRl] }` on an **authenticated** host (its `auth` set) so `auth("id")` keys per-user; on a public host `auth("id")` is null and every caller shares one bucket (`export()` warns — see request context above). **Shared-bucket rule:** co-attaching one middleware object to N hosts means all N share the *same* key ⇒ *one* counter — `max: 10` is a global per-user budget across them, not 10-per-host. Vary the key (fold in the host/action name) for an independent limit per host.
|
|
44
|
+
- **Middleware `exceptionPolicy`** governs what a **throw** in the middleware stack does to the request (XanoTS passes the value through; the Xano engine interprets it). `"rethrow"` is the **default** — the throw aborts the request and surfaces the authored `error`/status (a tripped `s.redis.ratelimit` → HTTP 429); the `post` chain still runs. `"silent"` swallows the throw, so a guard set to it is **not enforced** — advisory middleware only. `"critical"` is `"rethrow"` plus skipping the `post` chain. The only difference between `rethrow` and `critical` is whether `post` runs — no status or logging change.
|
|
45
|
+
|
|
46
|
+
- **Request history** controls per-object execution capture (the request/task/trigger debugger). Authored as a single scalar `history` field on any primitive: `false` off, `true` on at the default capture depth, a number = capture depth (how many statement executions are recorded per history record — NOT record retention), `"all"` unlimited. **Omit `history` to inherit** — the engine resolves object → container → workspace at request time (a query inherits from its API group, a tool from its toolset envelope, everything else straight from the workspace). Any authored value stops inheriting for that object. Per-kind defaults (when inheriting): query/task/tool capture ON, function/trigger/middleware OFF; default depth 100. Container tiers are authorable too — `apiGroup({ history })` sets the `query_*` default its queries inherit, and an agent/mcp_server/toolset `history` sets the `tool_*` default its tools inherit. Workspace-level defaults are the terminal tier: `workspaceConfig({ history: { query, function, task, tool, trigger, middleware } })` emits the flat `{objType}_enabled`/`{objType}_limit` map (no inherit flag) — setting it is wholesale (unlisted types fall back to their engine default), so declare every default you want to keep; omit the field to leave existing workspace history untouched.
|
|
47
|
+
|
|
48
|
+
- **Workspace environment variables** set a tenant's env vars through the workspace object: `workspaceConfig({ env: { STRIPE_KEY: process.env.STRIPE_KEY!, APP_BASE_URL: "https://…" } })`. Author them as a name→value MAP. Read a var back with `env("NAME")` (→ `$env.NAME`), which compiles to tag "setting" with the plain name. Values are SECRETS: prefer sourcing from `process.env` over committing literals, and don't commit a compiled bundle with real values. `deploy` REPLACES the tenant's env with the declared map; `release` (merge) is ADD-ONLY — it creates missing keys but does NOT update or remove existing ones, so changing a value in code and releasing leaves the live value unchanged. Omit `env` to leave existing env untouched. The separate `settings` field is a plain object.
|
package/llms/tests.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Saved unit tests, assertions, and mocks
|
|
2
|
+
|
|
3
|
+
> Read when authoring a `workflowTest()` stack, when a query/function/middleware carries `tests`, when a statement should mock a value, or when running a deployed environment's tests.
|
|
4
|
+
|
|
5
|
+
Named input sets run against one object, with assertions on its response — the tests the Xano editor shows. NOT `workflowTest()`, which is a standalone object with its own stack that calls other objects.
|
|
6
|
+
|
|
7
|
+
- `tests?: TestDef[]` on `query()`, `defineFunction()`, and `middleware()`. No other kind stores them.
|
|
8
|
+
- `{ name, id?, description?, datasource?, input?, expect?, token? }`.
|
|
9
|
+
- `name` is unique within the object and is the key a statement's `mock` resolves against; a duplicate throws.
|
|
10
|
+
- `id` defaults to a derivation from the owning object plus the name. A pulled test carries the id Xano minted, emitted explicitly.
|
|
11
|
+
- `datasource` defaults to `""` — an EMPTY datasource. Any other value names one the engine CLONES before the run; `"live"` warns at encode time.
|
|
12
|
+
- ⚠ That empty default means **no `table({ seed })` rows exist while a unit test runs**, exactly as for a `workflowTest()` — every `db` read misses and a count or index assertion (`resp("0.id")`) fails against a deployment whose endpoint returns those rows over HTTP a second later. Create what the test needs INSIDE the run — a `defineFunction` fixture the stack calls first — or `mock` the read.
|
|
13
|
+
- `input` is `{ <input name>: Value }` — tagged values (`c.*`, `ref`, …), never plain JS scalars.
|
|
14
|
+
- `token` runs the test as an authenticated caller. A pull does NOT bring it back: it is an expiring credential, reported as a deliberate omission.
|
|
15
|
+
|
|
16
|
+
### Assertions: `expect.*`, not `s.expect.*`
|
|
17
|
+
|
|
18
|
+
- `expect.*` builds a `{type, vars}` record stored on a `tests` entry. `s.expect.*` builds a STATEMENT for a `workflowTest()` stack. Different return types; not interchangeable.
|
|
19
|
+
- Subject first — argument order is the assertion: `expect.to_equal(resp(), c.int(2))`.
|
|
20
|
+
- `resp()` is the response under test; `resp("id")` drills, `resp("a-b")` takes the bracket escape.
|
|
21
|
+
- Subject only: `to_be_defined`, `to_not_be_defined`, `to_be_empty`, `to_be_null`, `to_not_be_null`, `to_be_true`, `to_be_false`, `to_be_in_the_future`, `to_be_in_the_past`.
|
|
22
|
+
- Subject + operand: `to_equal`, `to_not_equal`, `to_be_greater_than`, `to_be_less_than`, `to_contain`, `to_start_with`, `to_end_with`, `to_match` (operand is a delimiter-wrapped pattern — build it with `c.regex`).
|
|
23
|
+
- `expect.to_be_within(subject, min, max)` — both bounds EXCLUSIVE; a subject equal to either fails.
|
|
24
|
+
- `expect.to_throw(exception?)` takes NO subject. With an argument the error message must CONTAIN that text (case-insensitive substring); with none, any error passes.
|
|
25
|
+
|
|
26
|
+
### Mocks
|
|
27
|
+
|
|
28
|
+
A statement returns a value instead of doing its work, for one named test.
|
|
29
|
+
|
|
30
|
+
- `s.set_var("x", c.int(1), { mock: { "adds one": c.int(123) } })`. Every statement takes `mock`.
|
|
31
|
+
- Keyed by TEST NAME. A name the object does not declare THROWS at encode: the engine ignores a mock whose key is not a real test id, so it would deploy clean and silently never apply.
|
|
32
|
+
- `{ value, enabled: false }` keeps a mock stored but switched off.
|
|
33
|
+
- A mock applies ONLY while its test runs. It changes nothing about a normal request.
|
|
34
|
+
|
|
35
|
+
### `example`
|
|
36
|
+
|
|
37
|
+
`example?: { input?, output? }` on `query()` — the saved request/response sample the editor records. Free-form JSON, not tagged values. A pull DOES bring it back.
|
|
38
|
+
|
|
39
|
+
### What a `workflowTest()` run actually sees
|
|
40
|
+
|
|
41
|
+
The run is isolated in ways that make a correct test fail for reasons the failure message does not name.
|
|
42
|
+
|
|
43
|
+
- The run uses an EMPTY datasource, so **no `table({ seed })` rows exist while it runs** and every `db` read misses. A test that buys seeded row 1 fails with its own precondition message, which reads as a wrong id rather than an empty database. Create what the test needs INSIDE the test — typically a `defineFunction` fixture the stack calls first.
|
|
44
|
+
- `s.api.call` does NOT raise when the endpoint answers with an error. It BINDS the error envelope (`{code, message}`) to its `as` and carries on, so a later `s.expect.to_be_defined({ expr: ref("r.field") })` reports the ASSERTION while the real failure was the call, four statements up. Assert on the envelope — `s.expect.to_contain({ expr: ref("r.code"), value: c.text("ERROR_CODE_INPUT_ERROR") })` — when a call may fail. `s.function.run` raises instead; the two disagree.
|
|
45
|
+
- `s.expect.to_throw({ body, exception? })` runs `body` in an ISOLATED var stack, so a variable bound EARLIER in the test is not visible inside it — bind what the body needs inside the body. `exception` is text the raised message must CONTAIN; omit it to accept any error.
|
|
46
|
+
- `s.expect.to_throw` catches such a call only when the error carries a MESSAGE. `ERROR_CODE_ACCESS_DENIED` arrives with an empty one, so `to_throw` around an auth-refused call reports `to_throw failed - response is ok` — which reads as a broken auth gate on a gate that works.
|
|
47
|
+
- An endpoint's `auth` gate is NOT enforced on `s.api.call`. A `query({ auth: users })` runs anyway and fails only where its stack dereferences `auth(...)`. A stack that never touches `auth(...)` runs unauthenticated and passes.
|
|
48
|
+
- Neither `auth.token` nor an `Authorization` entry in `headers` authenticates the call — a token that answers 200 over real HTTP is refused here. To cover auth-gated logic, move the body into a `defineFunction` taking the user id and `s.function.call` that; the gate itself is not reachable from a workflow test.
|
|
49
|
+
|
|
50
|
+
### Running them
|
|
51
|
+
|
|
52
|
+
`xanots test run-all` runs the unit tests AND the `workflowTest()` objects an environment carries. It takes no entry file and compiles nothing: it runs what is DEPLOYED, so deploy before testing.
|
|
53
|
+
|
|
54
|
+
- `--dest ephemeral` (DEFAULT, `--name <env>` to pick one), `--dest sandbox`, or `--dest workspace`. Unlike `deploy`, `workspace` is allowed here — running a test reads.
|
|
55
|
+
- `xanots test list` shows what is there without running it; `xanots test run "<name>"` runs one. When a name is ambiguous the error prints the qualified `function:math/happy path` form, which `run` also accepts.
|
|
56
|
+
- `--kind unit|workflow` narrows to one family. `--concurrency <n>` defaults to 1: tests share the environment database.
|
|
57
|
+
- A failing suite exits 5, distinct from a crash. Tests that could not be REACHED exit 6 — retry that one, investigate the other. An environment with no tests is success, not failure.
|
|
58
|
+
- `xanots deploy ./index.ts --test` deploys and then runs the suite against what it just shipped. A failure exits 5 WITHOUT retracting the deploy — the environment is live either way.
|
package/llms/triggers.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Triggers
|
|
2
|
+
|
|
3
|
+
> Read when authoring any trigger. A trigger's `stack` is a callback rather than the plain array every other kind takes, so the shape does not carry over.
|
|
4
|
+
|
|
5
|
+
**A trigger's `stack` is a callback — `stack: (t) => [...]`, not the plain
|
|
6
|
+
`stack: []` array that `defineFunction`/`query`/`task` use.** That's the one
|
|
7
|
+
shape that doesn't carry over from the other kinds: a trigger has no
|
|
8
|
+
user-declared `input`, so its inputs are **implied by type** (fixed by Xano,
|
|
9
|
+
not editable) and arrive through the typed **stack handle** `t` — you can't
|
|
10
|
+
reference them without it. (Response-bearing types take `response: (t) =>
|
|
11
|
+
ResponseDef` too.) `t` exposes exactly that trigger type's inputs; a wrong
|
|
12
|
+
name is a compile error, not a runtime surprise. The seven trigger types are
|
|
13
|
+
distinct root factories (not a namespace): `{tableTrigger, realtimeServerTrigger,
|
|
14
|
+
realtimeChannelTrigger, mcpServerTrigger, agentTrigger, workspaceTrigger,
|
|
15
|
+
errorTrigger}({ name, guid?, description?, active?, tags?, ... })`.
|
|
16
|
+
|
|
17
|
+
- `tableTrigger({ name, table?, datasources?, actions?: {insert?,update?,delete?,truncate?}, stack })` — database/table trigger. `t.new` / `t.old` are the row **after** / **before** the change; `t.action` (`insert|update|delete|truncate`), `t.datasource`. Bind `table` to a `table()` handle and `t.new("col")` / `t.old("col")` are typed to that row (misspelled column = compile error). Nullability follows the enabled actions: insert → `old` is null, delete → `new` is null, update → both, truncate → neither. Config-only (no response).
|
|
18
|
+
- `realtimeServerTrigger({ name, realtimeServer, actions?: {connect?,disconnect?}, stack?, response?, responseShape? })` — realtime SERVER lifecycle (a client connecting to / disconnecting from the server, not a message). Inputs: `t.action` (`connect|disconnect`), `t.realtime_server`, `t.client`. Bind `realtimeServer` to a `realtimeServer()` handle (or its name). `connect` GATES the connection — a denial sends an `error` and CLOSES the socket with code 4401 before it is ever ready, so it is a real front door, not an observer; same return shape as a channel `join` (`{ allowed: c.bool(true) }` or any truthy value admits, EMPTY/FALSY DENIES — INCLUDING a gating trigger with NO `response`, which returns nothing and so refuses every client). A CRASH DENIES too — the transport seeds a deny and keeps it on a throw. Both failure modes lock the door, so plan for a self-inflicted LOCKOUT (an unguarded drill into a null `db.get` raises → everyone refused), not a breach. Gating is OPT-IN: a server with no `connect` trigger accepts every connection. `disconnect` is OBSERVATIONAL (return ignored, throws swallowed — cleanup must always complete). Both are SERVER-scoped, so `s.realtime.get_session` works but carries no channel path and no bound params.
|
|
19
|
+
- `realtimeChannelTrigger({ name, channel, actions?: {join?,leave?,deliver?}, stack?, response?, responseShape? })` — realtime CHANNEL lifecycle. Inputs: `t.action` (`join|leave|deliver`), `t.channel`, `t.client`. Bind `channel` to a `realtimeChannel()` handle — a bare path is NOT accepted (it is unique only within its server). The three actions have DIFFERENT postures, and the posture decides what the stack should return: `join` GATES the join (it runs BEFORE the client becomes a member, so a denial means it never sees a fan-out) — return `{ allowed: c.bool(true) }` (optional `reason` reaches the client) or any truthy value to admit, and an EMPTY OR FALSY RETURN DENIES, so a stack that just falls through — or a gating trigger with NO `response` — refuses everyone, and a CRASH DENIES too. That is the inverse of a normal message (a crashing message still delivers) and of `deliver` below (a gate that fails OPEN). `join`/`leave` bind the channel's typed path params as INPUTS, so `inp("room_id")` resolves and the gate decides per room; a SERVER connect/disconnect has no channel, the one place a path param cannot be read; `leave` is OBSERVATIONAL (return ignored, throws swallowed); `deliver` GATES delivery PER RECIPIENT — the per-viewer redaction tool and the most expensive action here (a stack per recipient per message), and it needs `delivery.perRecipient` on the channel to run at all — BOTH HALVES are required, so a `deliver` trigger on a channel without the flag NEVER RUNS and every subscriber receives the UNREDACTED payload (no error, no log line); `export()` warns on each half alone. **`deliver`'s RETURN VALUES DO NOT READ LIKE A FILTER:** ONLY an explicit NULL drops the message for that recipient; an OBJECT replaces that recipient's payload; ANYTHING ELSE — INCLUDING `false`, `0`, `""` — DELIVERS IT UNCHANGED, as does a crash. So `return false` from a yes/no redaction check SENDS the message it was written to suppress — return null instead. The delivered payload arrives NESTED, so read `inp("payload").<field>`, and `t.client` is the SENDER while `s.realtime.get_session` describes the RECIPIENT this run is for.
|
|
20
|
+
- `mcpServerTrigger({ name, mcpServer, stack?, response?, responseShape? })` / `agentTrigger({ name, agent, stack?, response?, responseShape? })` — toolset connection. Bind with the `mcpServer()`/`agent()` def handle (or its name) — it resolves to the toolset guid at export. Raw numeric `objId` is the escape hatch, rarely right: ids are assigned at import, so a handle passed to `objId` is a type error, and binding nothing deploys a trigger that never fires. Inputs: `t.toolset` (`t.toolset("name")`), `t.tools`. Response-bearing; the default stack copies `toolset`/`tools` into vars and returns them.
|
|
21
|
+
- `workspaceTrigger({ name, actions?: {branch_live?,branch_merge?,branch_new?}, stack? })` — branch lifecycle. Inputs: `t.to_branch`, `t.from_branch`, `t.action`. Config-only.
|
|
22
|
+
- `errorTrigger({ name, stack? })` — error-signature trigger. Inputs: `t.event` (`new|regression|fixed`), `t.id`, `t.signature`, `t.error` (`t.error("code")`/`t.error("message")`), `t.caller`, `t.statement`, `t.actor`, `t.count`, `t.first_seen`, `t.last_seen`, `t.fixed_at`. Config-only.
|
package/llms/values.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Value catalog
|
|
2
|
+
|
|
3
|
+
> Read when you need a literal, a reference, or a tag you have not used before — `c.*`, `ref`, `inp`, `auth`, `col`, and what each one encodes to.
|
|
4
|
+
|
|
5
|
+
- `c.text(s: string) => Value` — String constant → tag "const".
|
|
6
|
+
- `c.int(n: number | bigint | string) => Value` — Integer constant → tag "const:int". The engine stores integers as STRINGS and has no 53-bit limit, so pass a string or bigint for anything past Number.MAX_SAFE_INTEGER — c.int("18446744073709551615") is exact where the number literal for it is already …616. A number that is not a safe integer THROWS rather than encoding the rounded value.
|
|
7
|
+
- `c.decimal(n: number | string) => Value` — Decimal constant → tag "const:decimal". Pass a string only to keep a stored spelling a number cannot reproduce (c.decimal("10.00") keeps its trailing zeros).
|
|
8
|
+
- `c.blank(tag: "const:<type>") => Value` — The editor's UNCONFIGURED value box (stored value ""), emitted by codegen for a pulled workspace — do not author it. NOT a zero or an empty collection: the engine reads "" and "0" differently, so c.blank("const:int") ≠ c.int(0) and neither canonicalizes into the other. Constant tags except const/const:obj, whose blanks are c.text("")/c.obj(null).
|
|
9
|
+
- `c.bool(b: boolean) => Value` — Boolean constant → tag "const:bool".
|
|
10
|
+
- `c.null(tag?: "const:null" | "const:obj") => Value` — Null constant, stored value "null". Bare c.null() is tag "const:null". c.null("const:obj") is the OBJECT-TYPED null the engine writes into a db.* statement's @meta slot — different stored bytes from c.obj(null), which is the blank object (value ""), though both evaluate to null. Codegen emits whichever spelling the workspace stored; do not swap one for the other.
|
|
11
|
+
- `c.obj(o?: Json | null) => Value` — Object constant → tag "const:obj". A populated one stores an empty {} carrying one `set` filter per key — the editor's form, and the only populated form the engine reads back (a populated JSON string arrives truncated and fails the request with ERROR_FATAL "Unable to decode."). ⚠ a ZERO-BASED numeric key is an INDEX in the engine's data model, so c.obj({"0":"a"}) evaluates to the list ["a"] (a non-zero-based one like {"2":…} survives as a key) — that is the platform, not this encoding. No argument = the empty object {} — use this one. Explicit null = the legacy blank form the engine evaluates to null, NOT {}; it exists only so a pulled workspace round-trips, do not author it. Plain JSON literals only — a nested tagged value (inp/ref/auth/c.*) is rejected; for a computed object response use a record of values, not c.obj.
|
|
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
|
+
- `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
|
+
- `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.
|
|
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
|
+
- `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
|
+
- `col(name: string) => Value` — Reference a table column → tag "col".
|
|
19
|
+
- `auth(path?: string) => Value` — Reference the authenticated identity (auth("id") → $auth.id) → tag "auth".
|
|
20
|
+
- `caught(path?: "code" | "message" | "name" | "result") => Value` — Read the caught error inside an s.try_catch CATCH arm → tag "trycatch". Valid ONLY there — it reads empty in the try/finally arms and outside the statement. Those four fields are all the engine binds (result is the attached payload); bare caught() is the whole error record. ⚠ For an ENGINE-raised exception only `code` and `name` are populated; for an `s.throw`, `message` is the fixed string "Throw Error Statement" and your text is in `result`. So `caught("name")` is useful in both cases and `caught("message")` in NEITHER.
|
|
21
|
+
- `toolset(path: "token" | "params" | `params.${string}`) => Value` — Read a toolset-scoped binding inside a tool → tag "toolset". The engine binds two: token (the calling URL's token, null when absent) and params (its parameters, as an object); a dotted params.<key> reads one parameter out of that object. Bound only while a tool runs under its toolset — anywhere else it reads empty.
|
|
22
|
+
- `env(name: string) => Value` — Read a WORKSPACE environment variable (set via workspaceConfig({ env }) or the dashboard) → `$env.NAME`. Compiles to tag "setting" with the plain name. env("remote_ip") reads a user var named remote_ip, not the caller IP — use sys.remoteIp() for that.
|
|
23
|
+
- `setting(name: string) => Value` — Reference a workspace setting → tag "setting". Built-in system vars are $-prefixed settings, e.g. setting("$remote_ip"); prefer the typed sys.* accessors.
|
|
24
|
+
- `sys.*() => Value` — Built-in system / request-context variables → tag "setting" ($-prefixed). Accessors: remoteIp, requestMethod, requestUri, requestQueryString, httpHeaders, requestAuthToken, apiBaseUrl, datasource, branch, tenant, release, platform, isDebugger. In XanoScript these are $env.$remote_ip etc.; sys.remoteIp() is the public-endpoint rate-limit key (auth("id") is null there).
|
|
25
|
+
- `filter(name: string, ...args: Value[]) => FilterXdo` — Build a filter-chain entry by raw name (escape hatch).
|
|
26
|
+
- `fl.*(...args: Value[]) => FilterXdo` — Typed value-pipeline filters; see the `filters` catalog.
|
|
27
|
+
- `withFilters(value: Value, ...filters: FilterXdo[]) => Value` — Attach a filter chain to a value (filters passed spread; an array is also accepted).
|
|
28
|
+
|
|
29
|
+
Tags: const, const:int, const:decimal, const:bool, const:array, const:obj, const:null, const:epochms, const:expr, const:expr2, var, input, auth, env, setting, col, output, response, trycatch, toolset.
|