@xano/sdk 1.0.0

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.
Files changed (336) hide show
  1. package/CHANGELOG.md +788 -0
  2. package/LICENSE +21 -0
  3. package/README.md +541 -0
  4. package/dist/.build-fingerprint +1 -0
  5. package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
  6. package/dist/answer-shape-K67TAAPY.js +33 -0
  7. package/dist/backend-dir-2U53CIAL.js +39 -0
  8. package/dist/backend-slot-7QXW2QYC.js +28 -0
  9. package/dist/bin.d.ts +1 -0
  10. package/dist/bin.js +255 -0
  11. package/dist/branch-NCRNWQBQ.js +61 -0
  12. package/dist/branch-commands-ACIRCEQJ.js +79 -0
  13. package/dist/bundle.d.ts +223 -0
  14. package/dist/bundle.js +153 -0
  15. package/dist/capture-WAXPD7DR.js +41 -0
  16. package/dist/chunk-27GWNP2I.js +23 -0
  17. package/dist/chunk-2CMJZDCL.js +177 -0
  18. package/dist/chunk-2EZ3ZJZZ.js +302 -0
  19. package/dist/chunk-2GNFDZA2.js +390 -0
  20. package/dist/chunk-2MNLRIOX.js +11 -0
  21. package/dist/chunk-2TFRR6JV.js +104 -0
  22. package/dist/chunk-2ZUEE4EK.js +408 -0
  23. package/dist/chunk-36JUQZVQ.js +22 -0
  24. package/dist/chunk-3DRXWLJ5.js +83 -0
  25. package/dist/chunk-3JDU4VRL.js +137 -0
  26. package/dist/chunk-3KZCLN4Z.js +241 -0
  27. package/dist/chunk-3LDULPRE.js +168 -0
  28. package/dist/chunk-3M4LFYNQ.js +111 -0
  29. package/dist/chunk-3XI5CTUE.js +22 -0
  30. package/dist/chunk-3YW23UFH.js +50 -0
  31. package/dist/chunk-45UIT5T2.js +862 -0
  32. package/dist/chunk-4BW2SG2N.js +46 -0
  33. package/dist/chunk-4KHEMC32.js +79 -0
  34. package/dist/chunk-4POWHT2K.js +321 -0
  35. package/dist/chunk-4Q2GN7NW.js +192 -0
  36. package/dist/chunk-5A2PLKZB.js +70 -0
  37. package/dist/chunk-5FBYXK5P.js +159 -0
  38. package/dist/chunk-5HVXPTU4.js +323 -0
  39. package/dist/chunk-5QU2P5CA.js +304 -0
  40. package/dist/chunk-5V5XU32D.js +191 -0
  41. package/dist/chunk-5ZSO4T2G.js +10 -0
  42. package/dist/chunk-622HUCZT.js +10 -0
  43. package/dist/chunk-65HWOMGV.js +177 -0
  44. package/dist/chunk-6ELFRN3P.js +365 -0
  45. package/dist/chunk-6HO3NCSJ.js +1055 -0
  46. package/dist/chunk-6IUMKYWI.js +457 -0
  47. package/dist/chunk-6KEB3C3U.js +65 -0
  48. package/dist/chunk-6MR5TI5V.js +987 -0
  49. package/dist/chunk-6NNTNXN7.js +157 -0
  50. package/dist/chunk-6PBR37LN.js +97 -0
  51. package/dist/chunk-6QXLMMT3.js +155 -0
  52. package/dist/chunk-6ZGJCBZS.js +224 -0
  53. package/dist/chunk-73IVBKMA.js +250 -0
  54. package/dist/chunk-7G3CTU7I.js +286 -0
  55. package/dist/chunk-A3UDOZB6.js +222 -0
  56. package/dist/chunk-A6E77GDW.js +85 -0
  57. package/dist/chunk-AHJWCYQO.js +660 -0
  58. package/dist/chunk-AKNCFVKZ.js +36 -0
  59. package/dist/chunk-AM4FKL4J.js +237 -0
  60. package/dist/chunk-AN6BL47T.js +26 -0
  61. package/dist/chunk-AOSYVMUS.js +147 -0
  62. package/dist/chunk-APWXY5SL.js +182 -0
  63. package/dist/chunk-AWEIIR2U.js +218 -0
  64. package/dist/chunk-AWQ7BQTQ.js +73 -0
  65. package/dist/chunk-BAUZOWGX.js +393 -0
  66. package/dist/chunk-BHACRS6R.js +940 -0
  67. package/dist/chunk-BWPNT55T.js +680 -0
  68. package/dist/chunk-C6RIMRU4.js +187 -0
  69. package/dist/chunk-CGVQZXYG.js +263 -0
  70. package/dist/chunk-CIMQZRRE.js +162 -0
  71. package/dist/chunk-CJHQKJU4.js +113 -0
  72. package/dist/chunk-CTMO46PK.js +1897 -0
  73. package/dist/chunk-D53J55C5.js +106 -0
  74. package/dist/chunk-D7VGG3WS.js +90 -0
  75. package/dist/chunk-DAD3ONIS.js +245 -0
  76. package/dist/chunk-DGWHAVRG.js +162 -0
  77. package/dist/chunk-DX4QR4B2.js +483 -0
  78. package/dist/chunk-EHP3WPEG.js +21 -0
  79. package/dist/chunk-ELP2HMXC.js +432 -0
  80. package/dist/chunk-ERVIASGL.js +21 -0
  81. package/dist/chunk-ET4Y4XCB.js +865 -0
  82. package/dist/chunk-ETO6LGLD.js +4750 -0
  83. package/dist/chunk-EUUUMY7J.js +24 -0
  84. package/dist/chunk-FQJ2TTHP.js +218 -0
  85. package/dist/chunk-FQOF7E2E.js +133 -0
  86. package/dist/chunk-FWD7AL6N.js +21 -0
  87. package/dist/chunk-FXFV3KU6.js +138 -0
  88. package/dist/chunk-FXRQOWTE.js +171 -0
  89. package/dist/chunk-G554NZNI.js +210 -0
  90. package/dist/chunk-GSGDWJB5.js +13 -0
  91. package/dist/chunk-H2V3N23D.js +66 -0
  92. package/dist/chunk-H5C7MBCR.js +159 -0
  93. package/dist/chunk-HHN734BU.js +371 -0
  94. package/dist/chunk-HRVBLCVJ.js +243 -0
  95. package/dist/chunk-HVRUQXGD.js +1264 -0
  96. package/dist/chunk-I4F5TCJH.js +130 -0
  97. package/dist/chunk-IDOTCMDV.js +57 -0
  98. package/dist/chunk-IGOLHKWF.js +427 -0
  99. package/dist/chunk-JGE7CAWB.js +148 -0
  100. package/dist/chunk-JTUKLCXS.js +92 -0
  101. package/dist/chunk-JZGVVJSI.js +66 -0
  102. package/dist/chunk-K4WX6UUU.js +923 -0
  103. package/dist/chunk-K5BBGTYZ.js +217 -0
  104. package/dist/chunk-K6RXBGUB.js +427 -0
  105. package/dist/chunk-KTSDSD36.js +165 -0
  106. package/dist/chunk-L5JYN3NM.js +1038 -0
  107. package/dist/chunk-LIEOKXKD.js +354 -0
  108. package/dist/chunk-LLNZGEFK.js +51 -0
  109. package/dist/chunk-LPECSJ2J.js +55 -0
  110. package/dist/chunk-LQCMKAJ7.js +413 -0
  111. package/dist/chunk-LRN4ZJCQ.js +1145 -0
  112. package/dist/chunk-MJ4ZVHKN.js +17 -0
  113. package/dist/chunk-MXHSQOC4.js +3061 -0
  114. package/dist/chunk-N5GKCWE5.js +367 -0
  115. package/dist/chunk-NA3PXMEF.js +10 -0
  116. package/dist/chunk-NAQDMEHV.js +76 -0
  117. package/dist/chunk-NWKXM5RD.js +109 -0
  118. package/dist/chunk-NZOWWHU5.js +196 -0
  119. package/dist/chunk-OHX6MIUZ.js +184 -0
  120. package/dist/chunk-OOXWYAA4.js +15 -0
  121. package/dist/chunk-OPHB5ORL.js +1990 -0
  122. package/dist/chunk-OXA6PSZT.js +1725 -0
  123. package/dist/chunk-P2HAGF6H.js +73 -0
  124. package/dist/chunk-PTHVMXZI.js +3036 -0
  125. package/dist/chunk-PZ5AY32C.js +10 -0
  126. package/dist/chunk-Q325JKYB.js +189 -0
  127. package/dist/chunk-Q7DGTQ5G.js +255 -0
  128. package/dist/chunk-QCNYHBL7.js +601 -0
  129. package/dist/chunk-QO6BVGS5.js +145 -0
  130. package/dist/chunk-QXTKSOFI.js +76 -0
  131. package/dist/chunk-ROY4YAEQ.js +66 -0
  132. package/dist/chunk-RTJRZFFY.js +447 -0
  133. package/dist/chunk-SAVJUS2X.js +128 -0
  134. package/dist/chunk-SFLHVCKX.js +173 -0
  135. package/dist/chunk-STSFNCY4.js +127 -0
  136. package/dist/chunk-SWBF2LTK.js +170 -0
  137. package/dist/chunk-T5KELPAR.js +109 -0
  138. package/dist/chunk-TFFGOFVZ.js +1481 -0
  139. package/dist/chunk-TLHIFK4M.js +256 -0
  140. package/dist/chunk-TLQE7ZN3.js +156 -0
  141. package/dist/chunk-TPHWJ7UG.js +1981 -0
  142. package/dist/chunk-TRJCALW3.js +291 -0
  143. package/dist/chunk-TTEIYD5P.js +133 -0
  144. package/dist/chunk-U64O2MXY.js +407 -0
  145. package/dist/chunk-UCQ2VUTG.js +630 -0
  146. package/dist/chunk-UTI5G6OJ.js +44 -0
  147. package/dist/chunk-UVHDMMIP.js +188 -0
  148. package/dist/chunk-UZ6QQWJT.js +120 -0
  149. package/dist/chunk-VAX3UZOY.js +686 -0
  150. package/dist/chunk-W3HBUKFO.js +57 -0
  151. package/dist/chunk-WKQLGKCL.js +2823 -0
  152. package/dist/chunk-WQFNASK4.js +560 -0
  153. package/dist/chunk-WRBKLYSJ.js +224 -0
  154. package/dist/chunk-WTPZRR4I.js +17 -0
  155. package/dist/chunk-WZDJ2LLX.js +2385 -0
  156. package/dist/chunk-X2NL4KUO.js +14 -0
  157. package/dist/chunk-X4DVXBFY.js +38 -0
  158. package/dist/chunk-X677DMZM.js +7050 -0
  159. package/dist/chunk-XBKMWY7F.js +612 -0
  160. package/dist/chunk-XNCP4KYO.js +78 -0
  161. package/dist/chunk-XVBGLISP.js +35 -0
  162. package/dist/chunk-XW6KISBB.js +1192 -0
  163. package/dist/chunk-YGZHHAAB.js +8156 -0
  164. package/dist/chunk-YHEFQUQB.js +186 -0
  165. package/dist/chunk-YLDALTWZ.js +653 -0
  166. package/dist/chunk-YU4HSQW7.js +229 -0
  167. package/dist/chunk-ZDPP6RKB.js +27 -0
  168. package/dist/chunk-ZFDZKNGG.js +194 -0
  169. package/dist/chunk-ZIY7RNUT.js +49 -0
  170. package/dist/chunk-ZYJP6BOO.js +30 -0
  171. package/dist/cli.d.ts +1111 -0
  172. package/dist/cli.js +165 -0
  173. package/dist/codegen-command-VR2GWR6C.js +173 -0
  174. package/dist/codegen.d.ts +115 -0
  175. package/dist/codegen.js +19 -0
  176. package/dist/commands-GJMJ4XYZ.js +76 -0
  177. package/dist/completion-ZQLO6PB6.js +738 -0
  178. package/dist/config-MBYGCAFK.js +136 -0
  179. package/dist/deploy-command-JVZPWHMK.js +180 -0
  180. package/dist/deploy-source-3W45CXMP.js +81 -0
  181. package/dist/dev-env-sync-HN44K4D5.js +188 -0
  182. package/dist/diagnostics-UEAAHWVF.js +30 -0
  183. package/dist/display-name-hint-ABBPVQIR.js +46 -0
  184. package/dist/emit-CyhBDxli.d.ts +7007 -0
  185. package/dist/env-example-refresh-ET5HMMDW.js +126 -0
  186. package/dist/env-pull-command-TNMNB3CV.js +323 -0
  187. package/dist/env-target-O4HRKTGZ.js +40 -0
  188. package/dist/env-var-command-ZIRDCAZA.js +427 -0
  189. package/dist/ephemeral-P7F3PMAD.js +54 -0
  190. package/dist/ephemeral-command-CLJJKOFT.js +592 -0
  191. package/dist/ephemeral-state-PYCBSOGX.js +61 -0
  192. package/dist/file-lock-PT74GWYW.js +10 -0
  193. package/dist/generate-command-AYEOLA26.js +510 -0
  194. package/dist/gitignore-3SJ3WNRG.js +51 -0
  195. package/dist/guards-4EBNCZDC.js +162 -0
  196. package/dist/hosted-file-PSHJB3HX.js +28 -0
  197. package/dist/impersonate-command-5JAS3WSV.js +152 -0
  198. package/dist/import-EQSFU6JC.js +19 -0
  199. package/dist/index.d.ts +5470 -0
  200. package/dist/index.js +371 -0
  201. package/dist/init-command-X2FR4UAK.js +125 -0
  202. package/dist/init-modules-RY7UHAOL.js +50 -0
  203. package/dist/init-web-M4YZSZK6.js +42 -0
  204. package/dist/internal.d.ts +179 -0
  205. package/dist/internal.js +2634 -0
  206. package/dist/io-RC56QSN5.js +26 -0
  207. package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
  208. package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
  209. package/dist/landed-753YLJ5H.js +40 -0
  210. package/dist/landing-record-SU52SKXU.js +61 -0
  211. package/dist/live-diff-YONW2VI3.js +65 -0
  212. package/dist/local-engine-choice-CH5X3W7E.js +79 -0
  213. package/dist/local-engine-command-Z3OL7VOD.js +766 -0
  214. package/dist/local-engine-config-5U3SC434.js +39 -0
  215. package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
  216. package/dist/local-engine-process-EHTE5LLU.js +63 -0
  217. package/dist/local-engine-state-A4FTRFAF.js +32 -0
  218. package/dist/local-engine-update-check-6L645VBL.js +162 -0
  219. package/dist/lock-MRRHW5GS.js +87 -0
  220. package/dist/lock-commands-2QG25HL7.js +963 -0
  221. package/dist/login-command-QPYRWOXZ.js +538 -0
  222. package/dist/logout-command-WOIQLDP6.js +56 -0
  223. package/dist/loop-ALIZLOSX.js +191 -0
  224. package/dist/marketplace-command-K5YL42NO.js +977 -0
  225. package/dist/marketplace-resolve-LY32JES5.js +40 -0
  226. package/dist/meta-client-YXGPW53C.js +244 -0
  227. package/dist/module-manifest-JFGHU2TC.js +38 -0
  228. package/dist/near-key.d.ts +13 -0
  229. package/dist/near-key.js +8 -0
  230. package/dist/node.d.ts +95 -0
  231. package/dist/node.js +969 -0
  232. package/dist/operation-outcome-TZSESNC7.js +51 -0
  233. package/dist/output-target-AL3I44GG.js +47 -0
  234. package/dist/package-manager-PRAHUSCN.js +34 -0
  235. package/dist/plan-disclosure-6G4MLZKD.js +114 -0
  236. package/dist/plugin-contributions-WRNY5ABG.js +28 -0
  237. package/dist/plugin-prompt-4VA3B4FF.js +20 -0
  238. package/dist/plugin-questions-JTSYA74Q.js +24 -0
  239. package/dist/plugin.d.ts +396 -0
  240. package/dist/plugin.js +1 -0
  241. package/dist/preflight-command-2T5Z36MI.js +321 -0
  242. package/dist/profile-command-AH3PIM4X.js +800 -0
  243. package/dist/profile-pointer-GRWJU3YD.js +31 -0
  244. package/dist/profile-select-74TRDEPD.js +23 -0
  245. package/dist/project-dependency-KSH63ODT.js +35 -0
  246. package/dist/project-sdk-REPPOIBD.js +24 -0
  247. package/dist/promote-command-PCWT5N6S.js +140 -0
  248. package/dist/prompt-II3F3LND.js +27 -0
  249. package/dist/publish-command-TTDUUJI5.js +414 -0
  250. package/dist/pull-command-VBO7C4S3.js +161 -0
  251. package/dist/pull-superseded-GJWLG7XO.js +20 -0
  252. package/dist/release-command-N4ZPKMHR.js +146 -0
  253. package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
  254. package/dist/response-HlXahv4K.d.ts +925 -0
  255. package/dist/retry-command-AS5EVREX.js +34 -0
  256. package/dist/routes-manifest-SNWUVBT6.js +43 -0
  257. package/dist/runtime-XETHVF33.js +14 -0
  258. package/dist/scaffold.d.ts +1911 -0
  259. package/dist/scaffold.js +107 -0
  260. package/dist/secrets-file-LVZD6IIM.js +56 -0
  261. package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
  262. package/dist/seed-VU7C3QYS.js +46 -0
  263. package/dist/sent-writes-N7X4OVXT.js +22 -0
  264. package/dist/source-resolve-S2XA3XZF.js +83 -0
  265. package/dist/static-host-B2Y6AG3W.js +352 -0
  266. package/dist/status-command-RUDXTF7I.js +554 -0
  267. package/dist/store-6VNCD4D7.js +20 -0
  268. package/dist/store-ARM6VMO5.js +98 -0
  269. package/dist/store-DuYfzKlW.d.ts +3144 -0
  270. package/dist/suggest-6FOOYKHG.js +12 -0
  271. package/dist/sveltekit.d.ts +154 -0
  272. package/dist/sveltekit.js +109 -0
  273. package/dist/sync-contributions-UCEROO26.js +415 -0
  274. package/dist/table-ENAVMITR.js +42 -0
  275. package/dist/tables-command-GUPXXSB5.js +156 -0
  276. package/dist/tenant-IIHPOHGP.js +42 -0
  277. package/dist/tenant-command-D5HGI2X4.js +965 -0
  278. package/dist/test-command-TIKZMDPZ.js +476 -0
  279. package/dist/tests-BGGIOK3L.js +26 -0
  280. package/dist/token-NICJUTMT.js +72 -0
  281. package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
  282. package/dist/toolchain-modules-5EAZUONB.js +24 -0
  283. package/dist/tracked-backend-EIBUMDZA.js +80 -0
  284. package/dist/update-check-2W2NLDTC.js +136 -0
  285. package/dist/upgrade-command-QHNFWM7O.js +531 -0
  286. package/dist/verify-rollout-DRCK5TDD.js +61 -0
  287. package/dist/whoami-command-STGINTXH.js +51 -0
  288. package/dist/workspace-2CLHU2LK.js +93 -0
  289. package/dist/workspace-binding-56PIIPCP.js +54 -0
  290. package/dist/workspace-command-JEXQETJD.js +535 -0
  291. package/dist/workspace-env-F2RFMSC5.js +70 -0
  292. package/dist/workspace-export-OJSH2HIW.js +21 -0
  293. package/dist/writable-VA7OISSK.js +25 -0
  294. package/dist/xano-ICFUSPFR.js +42 -0
  295. package/dist/xanosdk-import-GN42TO2B.js +56 -0
  296. package/dist/xanosdk-project-S6DXJONQ.js +41 -0
  297. package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
  298. package/guides/README.md +33 -0
  299. package/guides/authoring.md +921 -0
  300. package/guides/cli.md +401 -0
  301. package/guides/codegen.md +177 -0
  302. package/guides/codes.md +404 -0
  303. package/guides/coverage.md +74 -0
  304. package/guides/deploying.md +1525 -0
  305. package/guides/environment.md +400 -0
  306. package/guides/marketplace.md +415 -0
  307. package/guides/module-contract.md +558 -0
  308. package/guides/object-kinds.md +433 -0
  309. package/guides/project-structure.md +43 -0
  310. package/guides/scaffold.md +351 -0
  311. package/guides/typed-frontend.md +242 -0
  312. package/llms/client.md +63 -0
  313. package/llms/errors.md +56 -0
  314. package/llms/fields.md +176 -0
  315. package/llms/filters.md +183 -0
  316. package/llms/kinds-agent-mcp.md +30 -0
  317. package/llms/kinds-core.md +100 -0
  318. package/llms/kinds-knowledge.md +20 -0
  319. package/llms/kinds-realtime.md +76 -0
  320. package/llms/lambda.md +94 -0
  321. package/llms/legacy.md +44 -0
  322. package/llms/lock.md +40 -0
  323. package/llms/object-kinds.md +34 -0
  324. package/llms/statements-calls.md +62 -0
  325. package/llms/statements-catalog.md +320 -0
  326. package/llms/statements-data.md +66 -0
  327. package/llms/statements-runtime.md +68 -0
  328. package/llms/tests.md +59 -0
  329. package/llms/toolchain.md +16 -0
  330. package/llms/triggers.md +36 -0
  331. package/llms/values.md +29 -0
  332. package/llms-full.txt +2085 -0
  333. package/llms.txt +493 -0
  334. package/manifest.json +11224 -0
  335. package/package.json +162 -0
  336. package/types/lambda-globals.d.ts +129 -0
@@ -0,0 +1,320 @@
1
+ # Statement catalog
2
+
3
+ > Read for the field signature of a specific statement — every surface, grouped by `s.*` namespace.
4
+
5
+ ### (top-level)
6
+
7
+ `s.throw` raises a named error `s.try_catch` catches; uncaught, it answers HTTP 200 `{ payload, statement: "Throw Error" }` (`s.precondition` sets a status). Inside `s.db.transaction` a throw or failed precondition rolls it back.
8
+
9
+ - `s.await({ as?: string; ids?: value; timeout?: value })`
10
+ - `s.foreach_break` [special]
11
+ - `s.comment` [special]
12
+ - `s.conditional` [special]
13
+ - `s.foreach_continue` [special]
14
+ - `s.for` [special]
15
+ - `s.foreach_remove` [special]
16
+ - `s.foreach` [special]
17
+ - `s.group` [special]
18
+ - `s.lambda({ as?: string; code: value; timeout?: value })`
19
+ - `s.precondition({ expr?: comparison; error_type?: "standard" | "notfound" | "toomanyrequests" | "accessdenied" | "unauthorized" | "badrequest" | "inputerror"; error?: value; payload?: value })`
20
+ - `s.return` [special]
21
+ - `s.switch` [special]
22
+ - `s.throw({ name?: string; value: value })`
23
+ - `s.try_catch` [special]
24
+ - `s.update_var` [special]
25
+ - `s.set_var` [special]
26
+ - `s.while` [special]
27
+
28
+ ### action
29
+
30
+ - `s.action.call` [special]
31
+ - `s.action.package.call` [special]
32
+
33
+ ### addon
34
+
35
+ - `s.addon.call` [special]
36
+
37
+ ### ai
38
+
39
+ - `s.ai.agent.run` [special]
40
+ - `s.ai.external.mcp.server_details({ as?: string; url: value; bearer_token?: value; connection_type?: "sse" | "stream" })`
41
+ - `s.ai.external.mcp.tool.list({ as?: string; url: value; bearer_token?: value; connection_type?: "sse" | "stream" })`
42
+ - `s.ai.external.mcp.tool.run({ as?: string; url: value; bearer_token?: value; connection_type?: "sse" | "stream"; tool: value; args?: value })`
43
+
44
+ ### api
45
+
46
+ - `s.api.call` [special]
47
+ - `s.api.request` [special, output]
48
+ - `s.api.stream({ value: value })`
49
+
50
+ ### array
51
+
52
+ - `s.array.difference({ as?: string; expr?: value; value?: value; by?: value })`
53
+ - `s.array.every({ expr?: value; as?: string; if?: comparison })` [output] → as: boolean
54
+ - `s.array.filter_count({ expr?: value; as?: string; if?: comparison })`
55
+ - `s.array.filter({ expr?: value; as?: string; if?: comparison })`
56
+ - `s.array.find_index({ expr?: value; as?: string; if?: comparison })`
57
+ - `s.array.find({ expr?: value; as?: string; if?: comparison })` [output]
58
+ - `s.array.group_by({ as?: string; expr?: value; by?: value })`
59
+ - `s.array.has({ expr?: value; as?: string; if?: comparison })`
60
+ - `s.array.intersection({ as?: string; expr?: value; value?: value; by?: value })`
61
+ - `s.array.map` [special]
62
+ - `s.array.merge({ name?: string; value?: value })` [output]
63
+ - `s.array.partition({ expr?: value; as?: string; if?: comparison })`
64
+ - `s.array.pop({ name?: string; as?: string })` [output]
65
+ - `s.array.push({ name?: string; value: value })` [output]
66
+ - `s.array.shift({ name?: string; as?: string })`
67
+ - `s.array.union` [special]
68
+ - `s.array.unshift({ name?: string; value: value })`
69
+
70
+ ### cloud
71
+
72
+ - `s.cloud.algolia.request({ as?: string; application_id: value; api_key: value; url: value; method?: "POST" | "GET" | "DELETE" | "PUT"; payload: value })` [output]
73
+ - `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 })`
74
+ - `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 })`
75
+ - `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 })`
76
+ - `s.cloud.aws.s3.delete_file({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value })`
77
+ - `s.cloud.aws.s3.get_file_info({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value })`
78
+ - `s.cloud.aws.s3.list_directory({ as?: string; bucket: value; region: value; key: value; secret: value; prefix?: value; next_page_token?: value })`
79
+ - `s.cloud.aws.s3.read_file({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value })`
80
+ - `s.cloud.aws.s3.sign_url({ as?: string; bucket: value; region: value; key: value; secret: value; file_key: value; ttl?: value })`
81
+ - `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 })`
82
+ - `s.cloud.azure.storage.delete_file({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value })`
83
+ - `s.cloud.azure.storage.get_file_info({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value })`
84
+ - `s.cloud.azure.storage.list_directory({ as?: string; account_name: value; account_key: value; container_name: value; path?: value })`
85
+ - `s.cloud.azure.storage.read_file({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value })`
86
+ - `s.cloud.azure.storage.sign_url({ as?: string; account_name: value; account_key: value; container_name: value; path: value; ttl?: value })`
87
+ - `s.cloud.azure.storage.upload_file({ as?: string; account_name: value; account_key: value; container_name: value; filePath: value; file: value; metadata?: value })`
88
+ - `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 })`
89
+ - `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 })`
90
+ - `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 })`
91
+ - `s.cloud.google.storage.delete_file({ as?: string; service_account: value; bucket: value; filePath: value })`
92
+ - `s.cloud.google.storage.get_file_info({ as?: string; service_account: value; bucket: value; filePath: value })`
93
+ - `s.cloud.google.storage.list_directory({ as?: string; service_account: value; bucket: value; path: value })`
94
+ - `s.cloud.google.storage.read_file({ as?: string; service_account: value; bucket: value; filePath: value })`
95
+ - `s.cloud.google.storage.sign_url({ as?: string; service_account: value; bucket: value; filePath: value; method?: "GET" | "POST"; ttl?: value })`
96
+ - `s.cloud.google.storage.upload_file({ as?: string; service_account: value; bucket: value; filePath: value; file: value; metadata?: value })`
97
+ - `s.cloud.job.await` [special]
98
+ - `s.cloud.job.status` [special]
99
+ - `s.cloud.job` [special]
100
+
101
+ ### datadog
102
+
103
+ - `s.datadog.log_bulk({ entries: value; connection?: value })`
104
+ - `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 })`
105
+ - `s.datadog.metric_bulk({ entries: value; connection?: value })`
106
+ - `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 })`
107
+
108
+ ### db
109
+
110
+ - `s.db.add_or_edit` [special] → as: InferRow<T> (upserts and never misses)
111
+ - `s.db.add` [special] → as: InferRow<T> (the full inserted row incl. id/created_at)
112
+ - `s.db.bulk.add` [special]
113
+ - `s.db.bulk.delete` [special] → as: number (count of deleted rows)
114
+ - `s.db.bulk.patch` [special] → as: InferRow<T>[]
115
+ - `s.db.bulk.update` [special]
116
+ - `s.db.del` [special] → as: null (the engine deletes and returns no value; throws NotFound on a miss)
117
+ - `s.db.direct_query` [special]
118
+ - `s.db.edit` [special] → as: InferRow<T> (the full post-mutation row; throws NotFound on a miss)
119
+ - `s.db.external.mssql.direct_query` [special]
120
+ - `s.db.external.mysql.direct_query` [special]
121
+ - `s.db.external.oracle.direct_query` [special]
122
+ - `s.db.external.postgres.direct_query` [special]
123
+ - `s.db.external.snowflake.direct_query` [special]
124
+ - `s.db.get` [special] → as: InferRow<T> | null (binds null on a miss, never throws)
125
+ - `s.db.has` [special] → as: boolean
126
+ - `s.db.increment` [special] → as: InferRow<T>[] | number (the updated rows, or the changed-row count with returnType "count")
127
+ - `s.db.patch` [special] → as: InferRow<T> (the full post-mutation row; throws NotFound on a miss)
128
+ - `s.db.query` [special] → as: InferRow<T>[] (a paging envelope when metadata paging is on)
129
+ - `s.db.schema` [special]
130
+ - `s.db.set_datasource({ value: value; workspace_id?: value })`
131
+ - `s.db.transaction` [special]
132
+ - `s.db.truncate` [special]
133
+
134
+ ### debug
135
+
136
+ `s.debug.stop` is a REAL terminator despite the namespace: it ends the request with an error value. Also exported flat as `die()`; there is no `s.die`.
137
+
138
+ - `s.debug.log({ value: value })`
139
+ - `s.debug.stop({ value: value })`
140
+
141
+ ### expect
142
+
143
+ Assertions. **Put them in a `workflowTest({...})` stack** — assert on what a `.call` bound with `as`. They are NOT inert elsewhere: a failure aborts the stack, so an `s.expect.*` in a `query`/`function`/`task` fails the request with HTTP 500 and the assertion's message (`to_equal failed - expected value 2 does not equal 1`) — remove it. `to_throw` sees only an error carrying a MESSAGE, so an empty-message failure (`ERROR_CODE_ACCESS_DENIED`) reports "response is ok", and `to_be_within` EXCLUDES both bounds — `min < expr < max` — while `s.security.random_number`'s are inclusive.
144
+
145
+ - `s.expect.to_be_defined({ expr?: value })`
146
+ - `s.expect.to_be_empty({ expr?: value })`
147
+ - `s.expect.to_be_false({ expr?: value })`
148
+ - `s.expect.to_be_greater_than({ expr?: value; value?: value })`
149
+ - `s.expect.to_be_in_the_future({ expr?: value })`
150
+ - `s.expect.to_be_in_the_past({ expr?: value })`
151
+ - `s.expect.to_be_less_than({ expr?: value; value?: value })`
152
+ - `s.expect.to_be_null({ expr?: value })`
153
+ - `s.expect.to_be_true({ expr?: value })`
154
+ - `s.expect.to_be_within({ expr?: value; min?: value; max?: value })`
155
+ - `s.expect.to_contain({ expr?: value; value?: value })`
156
+ - `s.expect.to_end_with({ expr?: value; value?: value })`
157
+ - `s.expect.to_equal({ expr?: value; value?: value })`
158
+ - `s.expect.to_match({ expr?: value; value?: value })`
159
+ - `s.expect.to_not_be_defined({ expr?: value })`
160
+ - `s.expect.to_not_be_null({ expr?: value })`
161
+ - `s.expect.to_not_equal({ expr?: value; value?: value })`
162
+ - `s.expect.to_start_with({ expr?: value; value?: value })`
163
+ - `s.expect.to_throw` [special]
164
+
165
+ ### function
166
+
167
+ - `s.function.call` [special]
168
+ - `s.function.run` [special]
169
+
170
+ ### math
171
+
172
+ - `s.math.add({ name?: string; value: value })` [output] → as: number
173
+ - `s.math.bitwise.and({ name?: string; value: value })` [output] → as: number
174
+ - `s.math.bitwise.or({ name?: string; value: value })` [output] → as: number
175
+ - `s.math.bitwise.xor({ name?: string; value: value })` [output] → as: number
176
+ - `s.math.div({ name?: string; value: value })`
177
+ - `s.math.mod({ name?: string; value: value })`
178
+ - `s.math.mul({ name?: string; value: value })`
179
+ - `s.math.sub({ name?: string; value: value })`
180
+
181
+ ### mcp
182
+
183
+ - `s.mcp.elicit` [special]
184
+ - `s.mcp.oauth.complete({ as?: string; request: value; decision: "approve" | "deny"; unsafe_user_id?: value })`
185
+ - `s.mcp.oauth.request({ as?: string; request: value })`
186
+ - `s.mcp.oauth.revoke({ as?: string; mcp_server: value; user_id?: value; grant_id?: value; all?: value })`
187
+ - `s.mcp.progress({ progress: value; total?: value; message?: value })`
188
+
189
+ ### microservice
190
+
191
+ - `s.microservice.request` [special]
192
+
193
+ ### middleware
194
+
195
+ - `s.middleware.call` [special]
196
+
197
+ ### object
198
+
199
+ - `s.object.entries({ as?: string; value?: value })` [output] → as: [string, unknown][]
200
+ - `s.object.keys({ as?: string; value?: value })` [output] → as: string[]
201
+ - `s.object.values({ as?: string; value?: value })` [output] → as: unknown[]
202
+
203
+ ### realtime
204
+
205
+ - `s.realtime.get_session({ as?: string })`
206
+ - `s.realtime.publish` [special]
207
+
208
+ ### redis
209
+
210
+ - `s.redis.count({ as?: string; key: value })`
211
+ - `s.redis.decr({ as?: string; key: value; by?: value })`
212
+ - `s.redis.del({ key: value })`
213
+ - `s.redis.get({ as?: string; key: value })` → as: unknown (FALSE (not null) on a miss)
214
+ - `s.redis.has({ as?: string; key: value })`
215
+ - `s.redis.incr({ as?: string; key: value; by?: value })`
216
+ - `s.redis.keys({ as?: string; search: value })`
217
+ - `s.redis.pop({ as?: string; key: value; count?: value })`
218
+ - `s.redis.push({ as?: string; key: value; value: value })`
219
+ - `s.redis.range({ as?: string; key: value; start?: value; stop?: value })`
220
+ - `s.redis.ratelimit({ as?: string; key: value; max?: value; ttl?: value; error?: value })`
221
+ - `s.redis.remove({ as?: string; key: value; value: value; count?: value })`
222
+ - `s.redis.set({ as?: string; key: value; data: value; ttl?: value; create_only?: value })` → as: boolean (false when create_only meets an existing key (kept))
223
+ - `s.redis.shift({ as?: string; key: value; count?: value })`
224
+ - `s.redis.unshift({ as?: string; key: value; value: value })`
225
+
226
+ ### security
227
+
228
+ - `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)
229
+ - `s.security.create_auth_token` [special]
230
+ - `s.security.create_curve_key({ as?: string; curve?: "P-256" | "P-384" | "P-521"; format?: "object" | "base64" })`
231
+ - `s.security.create_password({ as?: string; character_count?: value; require_lowercase?: value; require_uppercase?: value; require_digit?: value; require_symbol?: value; symbol_whitelist?: value })`
232
+ - `s.security.create_rsa_key({ as?: string; bits?: value; format?: "object" | "base64" })`
233
+ - `s.security.create_secret_key({ as?: string; bits?: value; format?: "object" | "base64" })`
234
+ - `s.security.create_uuid({ as?: string })`
235
+ - `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 })`
236
+ - `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 })`
237
+ - `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 })`
238
+ - `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 })`
239
+ - `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 })`
240
+ - `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 })`
241
+ - `s.security.random_bytes({ as?: string; length?: value })`
242
+ - `s.security.random_number({ as?: string; min?: value; max?: value })`
243
+
244
+ ### storage
245
+
246
+ - `s.storage.create_attachment({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value; include_meta?: boolean; type?: value })`
247
+ - `s.storage.create_audio({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value })` [output]
248
+ - `s.storage.create_file_resource({ as?: string; filename: value; filedata: value })` [output]
249
+ - `s.storage.create_image({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value })` [output]
250
+ - `s.storage.create_video({ as?: string; value: value; access?: "public" | "private" = "public"; filename?: value })` [output]
251
+ - `s.storage.delete_file({ pathname: value })` [output]
252
+ - `s.storage.read_file_resource({ as?: string; value: value })` → as: { name; size; mime; data: <contents> }
253
+ - `s.storage.sign_private_url({ as?: string; pathname: value; ttl?: value })`
254
+
255
+ ### stream
256
+
257
+ - `s.stream.from_csv({ as?: string; value: value; separator?: value; enclosure?: value; escape_char?: value })` [output]
258
+ - `s.stream.from_jsonl({ as?: string; value: value })`
259
+ - `s.stream.from_request` [special]
260
+
261
+ ### task
262
+
263
+ - `s.task.call` [special]
264
+
265
+ ### text
266
+
267
+ - `s.text.append({ name?: string; value: value })`
268
+ - `s.text.contains({ name?: string; as?: string; value: value })`
269
+ - `s.text.ends_with({ name?: string; as?: string; value: value })`
270
+ - `s.text.icontains({ name?: string; as?: string; value: value })`
271
+ - `s.text.iends_with({ name?: string; as?: string; value: value })`
272
+ - `s.text.istarts_with({ name?: string; as?: string; value: value })`
273
+ - `s.text.ltrim({ name?: string; value: value })`
274
+ - `s.text.prepend({ name?: string; value: value })`
275
+ - `s.text.rtrim({ name?: string; value: value })`
276
+ - `s.text.starts_with({ name?: string; as?: string; value: value })`
277
+ - `s.text.trim({ name?: string; value: value })`
278
+
279
+ ### tool
280
+
281
+ - `s.tool.call` [special]
282
+
283
+ ### trigger
284
+
285
+ - `s.trigger.call` [special]
286
+
287
+ ### util
288
+
289
+ `s.util.sleep`'s `value` is in SECONDS; a decimal is allowed (`c.decimal(0.5)` = 500 ms).
290
+
291
+ - `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)
292
+ - `s.util.get_all_input({ as?: string })` → as: unknown (inside an ATTACHED middleware it is a { type, vars } envelope whose vars DIFFERS BY PHASE: pre → the request inputs; post → { status, result } (plus payload on an error), NOT the request — read the request in post with s.util.get_raw_input. See llms/kinds-core.md)
293
+ - `s.util.get_env({ as?: string })`
294
+ - `s.util.get_input` [special]
295
+ - `s.util.get_raw_input` [special]
296
+ - `s.util.get_vars({ as?: string })`
297
+ - `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} } — ref("geo.location.latitude"), ref("geo.city.name"); radius is KILOMETRES. ⚠ Every leaf is nullable (region/city/postal often null on a normal hit). `city` is an OBJECT: a bare ref("geo.city") into a text column fails and { safe: true } does NOT help; drill to city.name. The whole var is null for an unresolvable address)
298
+ - `s.util.post_process` [special]
299
+ - `s.util.send_email({ as?: string; api_key?: value; service_provider?: "resend" | "xano"; subject?: value; message?: value; to?: value; bcc?: value; cc?: value; from?: value; reply_to?: value; scheduled_at?: value })`
300
+ - `s.util.set_header({ value: value; duplicates?: "replace" | "append" })`
301
+ - `s.util.sleep({ value: value })`
302
+ - `s.util.template_engine({ as?: string; value: value })`
303
+
304
+ ### webflow
305
+
306
+ - `s.webflow.request` [special]
307
+
308
+ ### workflow_test
309
+
310
+ Run another workflow test from inside one. Pass the `workflowTest()` def handle, not a name.
311
+
312
+ - `s.workflow_test.call` [special]
313
+
314
+ ### zip
315
+
316
+ - `s.zip.add_to_archive({ file: value; filename: value; zip: value; password?: value; password_encryption?: value })`
317
+ - `s.zip.create_archive({ as?: string; filename: value; password?: value; password_encryption?: "standard" | "AES-128" | "AES-192" | "AES-256" })`
318
+ - `s.zip.delete_from_archive({ filename: value; zip: value; password?: value })`
319
+ - `s.zip.extract({ as?: string; zip: value; password?: value })`
320
+ - `s.zip.view_contents({ as?: string; zip: value; password?: value })`
@@ -0,0 +1,66 @@
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. `with` is REQUIRED; omitted → `Unable to locate input: `.
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.has({ table, fieldName?, fieldValue, as? })` — existence test.
15
+ - `s.db.del({ table, fieldName?, fieldValue, as? })` — delete by field match.
16
+ - `s.db.add({ table, row?, data?, output?, as? })` — insert; `row` is a partial keyed by column.
17
+ - 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`.
18
+ - `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. A column's `nullable` is not consulted at encode; the engine refuses a null it forbids.
19
+ - Omitting a key on `add` writes the column's type default — declared `default` if set, `[]` for a list, `{}` for obj/json, else `null`. That `null` is EMITTED, not what the row holds: the engine applies the column's nullability, so a `nullable` column keeps `null` and a non-nullable one lands on its type's zero value (`""`, `0`, `false`). Set the cell when the stored value matters. On `edit` an omitted key keeps its stored value.
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
+ - A `table({ seed })` cell hashes the same way: the import writes the plaintext through the column's own rules, so a seeded credential matches under `security.check_password` exactly as an added one does. Demo accounts work as fixtures — the usual caution about seed data applies, since the plaintext sits in the repo.
22
+ - `s.db.edit({ table, fieldName?, fieldValue, row?, data?, output?, as? })` — update by field match.
23
+ - `s.db.patch({ table, fieldName?, fieldValue, data, output?, as? })` — merge a partial. ⚠ Unlike `db.edit`'s `row`, `data` is a single object `Value` — write `obj({ unread: c.int(0) })`, not the column-keyed record `row` takes.
24
+ On these three, `output` restricts the columns of the RETURNED row only — it does not change
25
+ what is written. Not offered on `db.del`/`db.has` (their result is a scalar) or on
26
+ `db.add_or_edit` (no output envelope).
27
+ - `s.db.add_or_edit({ table, fieldName?, fieldValue, row?, data?, as? })` — upsert.
28
+ - `s.db.query({ table, where?, additionalWhere?, bind?, sort?, paging?, external?, returnType?, distinct?, eval?, output?, lock?, addon?, as? })` — search.
29
+ - `where` / `additionalWhere` — `expr(...)`, an `expr[]` (ANDed), or a raw `Value`. Rides `context.search`.
30
+ - ⚠ `ignoreEmpty` DROPS the predicate when the operand is empty (not zero rows). On `in` an empty list returns the UNFILTERED set, so never scope to permitted ids with it; a `bulk.delete` only it scopes throws.
31
+ - 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.
32
+ - ⚠ `like`/`ilike` take the operand as the PATTERN, verbatim: a bare term matches only an exact whole-string equal, and the endpoint answers HTTP 200 with zero rows — nothing reports a problem, so a search box that matches nothing ships. For substring matching use `includes`/`not includes`, which wrap the operand in `%…%` themselves and match case-INSENSITIVELY. Prefer them over a hand-built `"%" + term + "%"`, which is non-empty even for an empty term and so defeats `ignoreEmpty`; `includes` composes with it. `contains`/`@>`/`overlaps` are JSON/array containment, not text — on a text column they 400 `ParseError: Invalid value for param`.
33
+ - Compose nested boolean logic with `and(...)` / `or(...)` groups (also available on `addon()` `where`).
34
+ - 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.
35
+ - ⚠ 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 (`qf.epochms_add_day(7)`, `qf.epochms_sub_month(1)`, `qf.epochms_year("UTC")`, …) — or compute it in an earlier `s.set_var` and `ref()` that. `export --strict` reports it; a bare `c.now()` operand is always fine.
36
+ - `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.
37
+ - ⚠ 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.
38
+ - `bind: [{ table: team, as: "team_row", join: "left", where: expr(col("team"), "=", col("team_row.id")) }]`
39
+ - ⚠ A join does NOT put the joined table's columns on the returned row — with or without a `bind`, a row is the QUERIED table's columns, which is what `InferResponse` types. There is no `row.team_row`. To read a joined column, PROJECT it with an `eval` whose `name` is the dotted path: `eval: [{ name: "team_row.name", as: "team_name" }]` puts `team_name` on the row and on the inferred type. A bare `name` there is `Unsupported parameter reference` at runtime (it qualifies to the base table), and a dotted joined column in `output` is dropped with no error.
40
+ - `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") }`.
41
+ - `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.
42
+ - An `eval`/`sort`/`where` filter pipeline compiles to **SQL**: `trim`/`lower`/`concat` work there too, plus a DIFFERENT registry than `fl.*`: the vector family, geo `distance`/`within`/`covers`, `search_rank`, the timestamp family, the aggregators. Build these with **`qf.*`** — `filters: [qf.vector_cos_distance(inp("q"))]` — which emits the same `{ name, arg }` step with the name, the argument COUNT and any enumerated argument checked at the call site. `qf` carries what the engine registers, including facts no name list holds: the per-part extractors take a timezone (`qf.epochms_month("UTC")`), `qf.round`'s precision is required, `qf.time`'s part is an enum. `QUERY_FILTER_NAMES`/`QUERY_AGGREGATE_NAMES` list the set; the raw `{ name, arg }` form still works for anything `qf` does not carry (`vector_distance`, which has no registered engine class).
43
+ - **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.
44
+ - `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`.
45
+ - ⚠ 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.
46
+ - 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.
47
+ - `sort: [{ sortBy: <col>, dir?: "asc"|"desc"|"rand" }]` and `paging: { page?, per_page?, offset?, totals?, metadata?, search?, sort? }` ride `context.return.list`.
48
+ - ⚠ `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. `paging: { metadata: false }` keeps the bare array.
49
+ - ⚠ Under the ENVELOPE, `output` selects from the envelope, not the row: write `output: ["itemsReceived", "curPage", "items.id", "items.title"]`. A bare column list matches no envelope key, so every key is dropped and the response is `[]` at HTTP 200 with no error (and `ref("<var>.items")` then fails on a var that is gone). With `metadata: false` the statement binds the rows, so bare columns are the right form there. `export()` warns on a row-rooted selection under an envelope; `--strict` fails it.
50
+ - Read `nextPage` (`number|null`) as the typed has-next signal.
51
+ - **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.
52
+ - A `search`/`sort`-only `paging` (no numeric field) does NOT paginate.
53
+ - `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.
54
+ - `distinct` — `"auto"` (default) | `"yes"` | `"no"`, riding `context.return.<list|stream>.distinct`.
55
+ - `s.db.truncate({ table, reset?, as? })` · `s.db.schema({ table, path, as? })`.
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.
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.
60
+ - `s.db.transaction({ body, as? })` — run a `Statement[]` atomically. `as` binds whatever the block returned.
61
+ - `s.db.bulk.add({ table, items, allowIdField?, as? })` / `s.db.bulk.update` / `s.db.bulk.patch` — `items` is an array `Value`.
62
+ - ⚠ `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).
63
+ - ⚠ **`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.
64
+ - `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 (missing, empty, or `x OR and()`) deletes **every** row, so it **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.
65
+ - `s.db.increment({ table, where, fieldName, value, returnType?, output?, addon?, as? })` — adds `value` (a number or `Value`; negative decrements) to one `int`/`decimal` column on every row `where` matches, in ONE atomic locked UPDATE. **Use it for counters, stock, balances and tallies**: `db.get` → math → `db.edit` loses updates under concurrency, and so do `db.bulk.patch`/`update`. A guard in `where` (`expr(col("stock"), ">=", c.int(1))`) is re-checked under the row lock. `returnType: "count"` binds the changed-row count instead of the rows, and then takes no `output`/`addon`.
66
+ - ⚠ A `where` that constrains nothing (missing, empty, or `x OR and()`) changes NO rows and **throws**. A null stored value counts as 0. A fractional `value` on an `int` column, a non-numeric or list column, the `id` key, and a result that overflows the column are rejected with no rows changed.
@@ -0,0 +1,68 @@
1
+ # Statement runtime behavior
2
+
3
+ > Read when you need to know what a statement's `as:` output holds, or why a bound variable is not the shape 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**. `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`**, not 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: HTTP 500 `ERROR_FATAL` "Duplicate record detected…", earlier writes kept. No error type is 409. `s.try_catch` catches it — and `s.precondition`/`guard.*` failures, answering 200 (`caught("code")`: `ERROR_CODE_NOT_FOUND`, …) — so re-raise with an `s.precondition` in `catch` to keep a status. No partial unique index: NULLs never collide, so null the key (or delete the row) to free it.
10
+ - **`InferResponse<typeof query>`** derives an endpoint's response type. 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.increment` | `Row[]` (updated rows), or `number` with `returnType: "count"` | `[]` / `0` |
19
+ | `db.del`, `db.bulk.add`/`bulk.update`, raw `direct_query` | `unknown` (the engine leaves them untyped) | — |
20
+ - 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`).
21
+ - A value reshaped by a lambda, or a variable built by control flow, also resolves to `unknown`.
22
+ - 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`; an async `s.function.run` binds its job id (`string`), not the result.
23
+ - In an object-literal response the KEYS are always known; a `c.*` constant VALUE types like an `s.set_var` of it (`{ success: c.bool(true) }` → `{ success: boolean }`).
24
+ - 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`).
25
+ - 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.
26
+ - 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.
27
+ - 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 bound ONLY inside branches → `responseShape` (a branch/case/loop/`try`/`catch` re-binding unions in; one in EVERY branch replaces); a call whose target is named by STRING → give the TARGET a `responseShape` (a handle then carries it) or declare it here. Use `responseShape` as the FIRST move here.
28
+ - An `s.set_var` holding a `c.*` CONSTANT brands its binding with that constant's JS type, so `ref("n")` after `s.set_var("n", c.int(1))` resolves to `number` (widened: `c.bool(false)` → `boolean`; `s.update_var` re-binds). A filter chain — the value's own or `asFilters` — then retypes it (`[fl.to_int()]` → `number`). A bare `ref`/`inp` stays `unknown`.
29
+ - ⚠ A hand-written `s.precondition` does not narrow the STATIC type. One (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. `guard.found("row")` DOES narrow: it emits the same non-null check (404) and drops `| null` from `row` — whole, dotted, and addon grafts alike — after it. `guard.owner` narrows the same way. Reach for `responseShape` only when neither fits.
30
+ - ⚠ 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. A `merge` one makes a LIST response an index-keyed object; its keys are not inferred.
31
+ - ⚠ **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.
32
+ - **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`.
33
+ - `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.
34
+ - `input` maps addon inputs — bind a parent-row column with `out(col)`. `output` restricts addon columns. `children` nests addons.
35
+ - The DEFINITION side — the `addon({...})` factory and `registerAddons` — is a def shape in `llms/kinds-core.md`.
36
+ - Attaching a typed `addon({ table, output })` handle merges its alias (the last `as` segment) onto the row in `InferResponse`: `{cols} | null` 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).
37
+ - An attachment-level `output` narrows an object/array graft further. A bare-NAME reference grafts `unknown` — narrow it at the call site.
38
+ - ⚠ An alias that shadows an existing column on the queried table throws at build time; rename with a `_` prefix.
39
+ - **Middleware attachment** runs a reusable `middleware({...})` before/after a host's own stack. Distinct from `s.middleware.call` (inline invoke).
40
+ - 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.
41
+ - 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).
42
+ - Prefer a def handle over a bare name when the middleware pins an explicit `guid`.
43
+ - `pre: middleware.clear()` (an empty list) overrides with nothing — stop inheriting.
44
+ - 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.
45
+ - **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 there is no caller identity for it to resolve.
46
+ - 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 `auth()` cannot resolve, so the request FAILS with a 403 on the first call, under `max`, with the host never running. Key off `sys.remoteIp()` on a public host.
47
+ - To catch that, `export()` **warns** (never blocks) when a middleware whose stack references `auth()` reaches 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.
48
+ - Inherited attachment (API group, workspace tier) warns too.
49
+ - **Rate-limit recipe (the canonical 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.") }) ] })`.
50
+ - `exceptionPolicy` defaults to `"rethrow"`, which is what makes a tripped limit abort with HTTP 429; `"silent"` would let the over-limit request through. Without `error` it never trips: it binds `false` and the request runs on (`export()` warns).
51
+ - Attach it with `middleware: { pre: [writeRl] }` on an **authenticated** host; on a public host key off `sys.remoteIp()`.
52
+ - **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.
53
+ - **Middleware `exceptionPolicy`** governs what a **throw** in the middleware stack does to the request. `"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.
54
+
55
+ - **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.
56
+ - **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.
57
+ - Per-kind defaults (when inheriting): query/task/tool capture ON, function/trigger/middleware OFF; default depth 100.
58
+ - 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.
59
+ - 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.
60
+
61
+ - **Workspace environment variables** set a tenant's env vars through the workspace object: `workspaceConfig({ env: { STRIPE_KEY: "", APP_BASE_URL: "" } })`, passed to `registerWorkspace()` — unattached, it declares nothing. Author them as a name→value MAP, declaring each NAME with an EMPTY value. Read a var back with `env("NAME")` (→ `$env.NAME`), which compiles to tag "setting" with the plain name.
62
+ - Values are SECRETS and do NOT belong here: they live in `xano/.env`, which is gitignored and survives a `xanosdk pull`, and every command that compiles a bundle reads it with no flag. Fill it by hand from `xano/.env.example`, or run `xanosdk env pull`. CI passes `--backend-env-file <path>` or `--env-var KEY=VALUE` instead. A compiled bundle carries the values in cleartext — don't commit one.
63
+ - What a deploy sends is the RESOLVED map (the declared names, then ONE file — `xano/.env`, or `--backend-env-file <path>` which REPLACES it, never both — then `--env-var`), not the literal object written here. `deploy` REPLACES the tenant's env with it, so a declared name with no value anywhere REFUSES the deploy rather than clearing the live value — fill the name in rather than putting the value back in source, and reach for `--allow-empty-env=NAME` only when clearing is what you mean.
64
+
65
+ - **Workspace documentation** gates the workspace's hosted docs: `workspaceConfig({ documentation: { require_token: true } })`. `require_token: true` IS the declaration that a token exists; the value lives in `xano/.secrets.json`, keyed by the object it gates rather than by a name, and a literal `token` FAILS the export. It is NOT a backend env var — `env()` cannot read it. Other members are carried verbatim.
66
+ - ⚠ OMIT the block and NO `documentation` key is emitted, leaving the target's live block as it is; the legacy `realtime` block follows the same rule. An `apiGroup`'s `documentation` follows the OPPOSITE rule — see its def shape.
67
+ - A declared gate with no token emits no block at all and REFUSES a `deploy`; `--allow-empty-doc-token=<scope>` clears that gate deliberately (scope = `workspace`, or an API group's NAME). `xanosdk pull` writes `xano/.secrets.json`, and `--no-secrets` writes none.
68
+ - A merge (`deploy --to`) is ADD-ONLY, matched BY NAME — it creates names the target lacks but does NOT update or remove ones it has, so the target's value wins. Two gaps: a release carries no env values, so a NEW name reads null after `promote`/`tenant deploy` until `env set` (both name it first) — and a `--replace` deploy rebuilds it so every env var the target holds that the config does not declare is DROPPED (named, and confirmed, before it runs). Omit `env` entirely to leave existing env untouched. The separate `settings` field is a plain object.
package/llms/tests.md ADDED
@@ -0,0 +1,59 @@
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 needs a `mock`, 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 `test.live-datasource` on the owning def.
12
+ - ⚠ That empty default means **no `table({ seed })` rows exist while a unit test runs**, exactly as for a `workflowTest()` — every `db` read misses, so `resp("0.id")` fails though the deployed endpoint returns those rows. **To read `table({ seed })` rows on an EPHEMERAL**, set `datasource: "live"`: there it holds only the seed fixtures (plus rows `--keep-data` kept); the run's writes are dropped. `diagnostics.allow` accepts its warning. ⚠ `datasource` is STORED, not a per-run flag — a test left on `"live"` clones the REAL database once it runs against an instance, so clear it before you promote. Otherwise create the rows 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. `exception` is a `Value` — `expect.to_throw(c.text("Wrong password"))`, not a bare string (a bare one fails to compile with TS2345).
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 by default (`datasource: ""`), so **no `table({ seed })` rows exist while it runs** (same as a unit test, above): a read of seeded row 1 fails with its own precondition message, not an empty-database error. Build fixtures INSIDE the test, or set `datasource: "live"` under the ephemeral-only rule above.
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. An outer one raises `Missing var entry: <name>` there, which the test reports as `to_throw` not matching (`export()` warns). `exception` is a `Value` whose text the raised message must CONTAIN (`c.text("already exists")`, not a bare string); omit it to accept any error.
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
+ `xanosdk 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
+ - `--on <backend>` picks the backend — `--on ephemeral:<name>`, `--on local-engine`, `--on tenant:<name>`, `--on workspace`; omitted, the one this project last deployed to (grammar: **Backends** in `llms.txt`). ⚠ `workspace` runs against the REAL database: safe only while every test keeps `datasource: ""`, since a stored non-empty datasource is CLONED before the run.
55
+ - `xanosdk test list` shows what is there without running it; `test run "<name>"` runs one; an ambiguous name prints qualified forms (`function:math/happy path`, `workflow:<name>`) `run` 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; exit 6 when the suite could not be reached — retry it; a backend that can't be looked up exits 8 (see errors). Having no tests is success.
58
+ - For CI: the exit code says THAT something failed, the JSON says WHICH. Progress goes to stderr; stdout carries one JSON document when not a terminal, or with `--json`. `run-all` and `run`: `{ kind, env, name, display, total, passed, failed, tests: [{ kind, name, qualified, object?, status: "pass"|"fail", message?, expectations?: [{ index, status, message? }], timing? }] }` (`message`: the first failure; `expectations`: a unit test's every expectation in order, `index` from 0; `env`: an ephemeral's or tenant's name; `name`: the backend's own, a local engine's too; null if none), same keys on an empty suite. `list` is `{ kind, env, name, display, total, tests }`; `deploy --test` nests the run under `testRun`.
59
+ - `xanosdk 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.
@@ -0,0 +1,16 @@
1
+ # Toolchain modules
2
+
3
+ > Read when `package.json` has a `"xanosdk"` block or you run `xanosdk marketplace`.
4
+
5
+ **Toolchain modules** extend the CLI, not the workspace — nothing to register in
6
+ `xano/index.ts`. An installed one runs on `export` and `deploy` (after the compile,
7
+ before the upload) and is verified, never rewritten, under `--frozen-lock`. Config
8
+ lives in this project's `package.json` `"xanosdk"` block, keyed by package name.
9
+ ⚠ `marketplace install <pkg>` is not just `npm install`: it asks the module its questions
10
+ and writes that block plus the `.gitattributes` lines it contributes. A module that
11
+ arrived any other way (a plain `npm install`, a merged PR) is configured by running that
12
+ same command on it; until then `export`/`deploy` report it unconfigured and it runs on
13
+ shipped defaults. `marketplace reinstall <pkg>` re-asks with the current settings as the
14
+ defaults and re-enables a disabled one; `marketplace remove <pkg>` drops block, lines and
15
+ dependency together — a plain `npm uninstall` leaves the block, and a package it names
16
+ that is no longer installed fails every `--frozen-lock` run.