@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.
- package/CHANGELOG.md +788 -0
- package/LICENSE +21 -0
- package/README.md +541 -0
- package/dist/.build-fingerprint +1 -0
- package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
- package/dist/answer-shape-K67TAAPY.js +33 -0
- package/dist/backend-dir-2U53CIAL.js +39 -0
- package/dist/backend-slot-7QXW2QYC.js +28 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +255 -0
- package/dist/branch-NCRNWQBQ.js +61 -0
- package/dist/branch-commands-ACIRCEQJ.js +79 -0
- package/dist/bundle.d.ts +223 -0
- package/dist/bundle.js +153 -0
- package/dist/capture-WAXPD7DR.js +41 -0
- package/dist/chunk-27GWNP2I.js +23 -0
- package/dist/chunk-2CMJZDCL.js +177 -0
- package/dist/chunk-2EZ3ZJZZ.js +302 -0
- package/dist/chunk-2GNFDZA2.js +390 -0
- package/dist/chunk-2MNLRIOX.js +11 -0
- package/dist/chunk-2TFRR6JV.js +104 -0
- package/dist/chunk-2ZUEE4EK.js +408 -0
- package/dist/chunk-36JUQZVQ.js +22 -0
- package/dist/chunk-3DRXWLJ5.js +83 -0
- package/dist/chunk-3JDU4VRL.js +137 -0
- package/dist/chunk-3KZCLN4Z.js +241 -0
- package/dist/chunk-3LDULPRE.js +168 -0
- package/dist/chunk-3M4LFYNQ.js +111 -0
- package/dist/chunk-3XI5CTUE.js +22 -0
- package/dist/chunk-3YW23UFH.js +50 -0
- package/dist/chunk-45UIT5T2.js +862 -0
- package/dist/chunk-4BW2SG2N.js +46 -0
- package/dist/chunk-4KHEMC32.js +79 -0
- package/dist/chunk-4POWHT2K.js +321 -0
- package/dist/chunk-4Q2GN7NW.js +192 -0
- package/dist/chunk-5A2PLKZB.js +70 -0
- package/dist/chunk-5FBYXK5P.js +159 -0
- package/dist/chunk-5HVXPTU4.js +323 -0
- package/dist/chunk-5QU2P5CA.js +304 -0
- package/dist/chunk-5V5XU32D.js +191 -0
- package/dist/chunk-5ZSO4T2G.js +10 -0
- package/dist/chunk-622HUCZT.js +10 -0
- package/dist/chunk-65HWOMGV.js +177 -0
- package/dist/chunk-6ELFRN3P.js +365 -0
- package/dist/chunk-6HO3NCSJ.js +1055 -0
- package/dist/chunk-6IUMKYWI.js +457 -0
- package/dist/chunk-6KEB3C3U.js +65 -0
- package/dist/chunk-6MR5TI5V.js +987 -0
- package/dist/chunk-6NNTNXN7.js +157 -0
- package/dist/chunk-6PBR37LN.js +97 -0
- package/dist/chunk-6QXLMMT3.js +155 -0
- package/dist/chunk-6ZGJCBZS.js +224 -0
- package/dist/chunk-73IVBKMA.js +250 -0
- package/dist/chunk-7G3CTU7I.js +286 -0
- package/dist/chunk-A3UDOZB6.js +222 -0
- package/dist/chunk-A6E77GDW.js +85 -0
- package/dist/chunk-AHJWCYQO.js +660 -0
- package/dist/chunk-AKNCFVKZ.js +36 -0
- package/dist/chunk-AM4FKL4J.js +237 -0
- package/dist/chunk-AN6BL47T.js +26 -0
- package/dist/chunk-AOSYVMUS.js +147 -0
- package/dist/chunk-APWXY5SL.js +182 -0
- package/dist/chunk-AWEIIR2U.js +218 -0
- package/dist/chunk-AWQ7BQTQ.js +73 -0
- package/dist/chunk-BAUZOWGX.js +393 -0
- package/dist/chunk-BHACRS6R.js +940 -0
- package/dist/chunk-BWPNT55T.js +680 -0
- package/dist/chunk-C6RIMRU4.js +187 -0
- package/dist/chunk-CGVQZXYG.js +263 -0
- package/dist/chunk-CIMQZRRE.js +162 -0
- package/dist/chunk-CJHQKJU4.js +113 -0
- package/dist/chunk-CTMO46PK.js +1897 -0
- package/dist/chunk-D53J55C5.js +106 -0
- package/dist/chunk-D7VGG3WS.js +90 -0
- package/dist/chunk-DAD3ONIS.js +245 -0
- package/dist/chunk-DGWHAVRG.js +162 -0
- package/dist/chunk-DX4QR4B2.js +483 -0
- package/dist/chunk-EHP3WPEG.js +21 -0
- package/dist/chunk-ELP2HMXC.js +432 -0
- package/dist/chunk-ERVIASGL.js +21 -0
- package/dist/chunk-ET4Y4XCB.js +865 -0
- package/dist/chunk-ETO6LGLD.js +4750 -0
- package/dist/chunk-EUUUMY7J.js +24 -0
- package/dist/chunk-FQJ2TTHP.js +218 -0
- package/dist/chunk-FQOF7E2E.js +133 -0
- package/dist/chunk-FWD7AL6N.js +21 -0
- package/dist/chunk-FXFV3KU6.js +138 -0
- package/dist/chunk-FXRQOWTE.js +171 -0
- package/dist/chunk-G554NZNI.js +210 -0
- package/dist/chunk-GSGDWJB5.js +13 -0
- package/dist/chunk-H2V3N23D.js +66 -0
- package/dist/chunk-H5C7MBCR.js +159 -0
- package/dist/chunk-HHN734BU.js +371 -0
- package/dist/chunk-HRVBLCVJ.js +243 -0
- package/dist/chunk-HVRUQXGD.js +1264 -0
- package/dist/chunk-I4F5TCJH.js +130 -0
- package/dist/chunk-IDOTCMDV.js +57 -0
- package/dist/chunk-IGOLHKWF.js +427 -0
- package/dist/chunk-JGE7CAWB.js +148 -0
- package/dist/chunk-JTUKLCXS.js +92 -0
- package/dist/chunk-JZGVVJSI.js +66 -0
- package/dist/chunk-K4WX6UUU.js +923 -0
- package/dist/chunk-K5BBGTYZ.js +217 -0
- package/dist/chunk-K6RXBGUB.js +427 -0
- package/dist/chunk-KTSDSD36.js +165 -0
- package/dist/chunk-L5JYN3NM.js +1038 -0
- package/dist/chunk-LIEOKXKD.js +354 -0
- package/dist/chunk-LLNZGEFK.js +51 -0
- package/dist/chunk-LPECSJ2J.js +55 -0
- package/dist/chunk-LQCMKAJ7.js +413 -0
- package/dist/chunk-LRN4ZJCQ.js +1145 -0
- package/dist/chunk-MJ4ZVHKN.js +17 -0
- package/dist/chunk-MXHSQOC4.js +3061 -0
- package/dist/chunk-N5GKCWE5.js +367 -0
- package/dist/chunk-NA3PXMEF.js +10 -0
- package/dist/chunk-NAQDMEHV.js +76 -0
- package/dist/chunk-NWKXM5RD.js +109 -0
- package/dist/chunk-NZOWWHU5.js +196 -0
- package/dist/chunk-OHX6MIUZ.js +184 -0
- package/dist/chunk-OOXWYAA4.js +15 -0
- package/dist/chunk-OPHB5ORL.js +1990 -0
- package/dist/chunk-OXA6PSZT.js +1725 -0
- package/dist/chunk-P2HAGF6H.js +73 -0
- package/dist/chunk-PTHVMXZI.js +3036 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q325JKYB.js +189 -0
- package/dist/chunk-Q7DGTQ5G.js +255 -0
- package/dist/chunk-QCNYHBL7.js +601 -0
- package/dist/chunk-QO6BVGS5.js +145 -0
- package/dist/chunk-QXTKSOFI.js +76 -0
- package/dist/chunk-ROY4YAEQ.js +66 -0
- package/dist/chunk-RTJRZFFY.js +447 -0
- package/dist/chunk-SAVJUS2X.js +128 -0
- package/dist/chunk-SFLHVCKX.js +173 -0
- package/dist/chunk-STSFNCY4.js +127 -0
- package/dist/chunk-SWBF2LTK.js +170 -0
- package/dist/chunk-T5KELPAR.js +109 -0
- package/dist/chunk-TFFGOFVZ.js +1481 -0
- package/dist/chunk-TLHIFK4M.js +256 -0
- package/dist/chunk-TLQE7ZN3.js +156 -0
- package/dist/chunk-TPHWJ7UG.js +1981 -0
- package/dist/chunk-TRJCALW3.js +291 -0
- package/dist/chunk-TTEIYD5P.js +133 -0
- package/dist/chunk-U64O2MXY.js +407 -0
- package/dist/chunk-UCQ2VUTG.js +630 -0
- package/dist/chunk-UTI5G6OJ.js +44 -0
- package/dist/chunk-UVHDMMIP.js +188 -0
- package/dist/chunk-UZ6QQWJT.js +120 -0
- package/dist/chunk-VAX3UZOY.js +686 -0
- package/dist/chunk-W3HBUKFO.js +57 -0
- package/dist/chunk-WKQLGKCL.js +2823 -0
- package/dist/chunk-WQFNASK4.js +560 -0
- package/dist/chunk-WRBKLYSJ.js +224 -0
- package/dist/chunk-WTPZRR4I.js +17 -0
- package/dist/chunk-WZDJ2LLX.js +2385 -0
- package/dist/chunk-X2NL4KUO.js +14 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-X677DMZM.js +7050 -0
- package/dist/chunk-XBKMWY7F.js +612 -0
- package/dist/chunk-XNCP4KYO.js +78 -0
- package/dist/chunk-XVBGLISP.js +35 -0
- package/dist/chunk-XW6KISBB.js +1192 -0
- package/dist/chunk-YGZHHAAB.js +8156 -0
- package/dist/chunk-YHEFQUQB.js +186 -0
- package/dist/chunk-YLDALTWZ.js +653 -0
- package/dist/chunk-YU4HSQW7.js +229 -0
- package/dist/chunk-ZDPP6RKB.js +27 -0
- package/dist/chunk-ZFDZKNGG.js +194 -0
- package/dist/chunk-ZIY7RNUT.js +49 -0
- package/dist/chunk-ZYJP6BOO.js +30 -0
- package/dist/cli.d.ts +1111 -0
- package/dist/cli.js +165 -0
- package/dist/codegen-command-VR2GWR6C.js +173 -0
- package/dist/codegen.d.ts +115 -0
- package/dist/codegen.js +19 -0
- package/dist/commands-GJMJ4XYZ.js +76 -0
- package/dist/completion-ZQLO6PB6.js +738 -0
- package/dist/config-MBYGCAFK.js +136 -0
- package/dist/deploy-command-JVZPWHMK.js +180 -0
- package/dist/deploy-source-3W45CXMP.js +81 -0
- package/dist/dev-env-sync-HN44K4D5.js +188 -0
- package/dist/diagnostics-UEAAHWVF.js +30 -0
- package/dist/display-name-hint-ABBPVQIR.js +46 -0
- package/dist/emit-CyhBDxli.d.ts +7007 -0
- package/dist/env-example-refresh-ET5HMMDW.js +126 -0
- package/dist/env-pull-command-TNMNB3CV.js +323 -0
- package/dist/env-target-O4HRKTGZ.js +40 -0
- package/dist/env-var-command-ZIRDCAZA.js +427 -0
- package/dist/ephemeral-P7F3PMAD.js +54 -0
- package/dist/ephemeral-command-CLJJKOFT.js +592 -0
- package/dist/ephemeral-state-PYCBSOGX.js +61 -0
- package/dist/file-lock-PT74GWYW.js +10 -0
- package/dist/generate-command-AYEOLA26.js +510 -0
- package/dist/gitignore-3SJ3WNRG.js +51 -0
- package/dist/guards-4EBNCZDC.js +162 -0
- package/dist/hosted-file-PSHJB3HX.js +28 -0
- package/dist/impersonate-command-5JAS3WSV.js +152 -0
- package/dist/import-EQSFU6JC.js +19 -0
- package/dist/index.d.ts +5470 -0
- package/dist/index.js +371 -0
- package/dist/init-command-X2FR4UAK.js +125 -0
- package/dist/init-modules-RY7UHAOL.js +50 -0
- package/dist/init-web-M4YZSZK6.js +42 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +2634 -0
- package/dist/io-RC56QSN5.js +26 -0
- package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
- package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
- package/dist/landed-753YLJ5H.js +40 -0
- package/dist/landing-record-SU52SKXU.js +61 -0
- package/dist/live-diff-YONW2VI3.js +65 -0
- package/dist/local-engine-choice-CH5X3W7E.js +79 -0
- package/dist/local-engine-command-Z3OL7VOD.js +766 -0
- package/dist/local-engine-config-5U3SC434.js +39 -0
- package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
- package/dist/local-engine-process-EHTE5LLU.js +63 -0
- package/dist/local-engine-state-A4FTRFAF.js +32 -0
- package/dist/local-engine-update-check-6L645VBL.js +162 -0
- package/dist/lock-MRRHW5GS.js +87 -0
- package/dist/lock-commands-2QG25HL7.js +963 -0
- package/dist/login-command-QPYRWOXZ.js +538 -0
- package/dist/logout-command-WOIQLDP6.js +56 -0
- package/dist/loop-ALIZLOSX.js +191 -0
- package/dist/marketplace-command-K5YL42NO.js +977 -0
- package/dist/marketplace-resolve-LY32JES5.js +40 -0
- package/dist/meta-client-YXGPW53C.js +244 -0
- package/dist/module-manifest-JFGHU2TC.js +38 -0
- package/dist/near-key.d.ts +13 -0
- package/dist/near-key.js +8 -0
- package/dist/node.d.ts +95 -0
- package/dist/node.js +969 -0
- package/dist/operation-outcome-TZSESNC7.js +51 -0
- package/dist/output-target-AL3I44GG.js +47 -0
- package/dist/package-manager-PRAHUSCN.js +34 -0
- package/dist/plan-disclosure-6G4MLZKD.js +114 -0
- package/dist/plugin-contributions-WRNY5ABG.js +28 -0
- package/dist/plugin-prompt-4VA3B4FF.js +20 -0
- package/dist/plugin-questions-JTSYA74Q.js +24 -0
- package/dist/plugin.d.ts +396 -0
- package/dist/plugin.js +1 -0
- package/dist/preflight-command-2T5Z36MI.js +321 -0
- package/dist/profile-command-AH3PIM4X.js +800 -0
- package/dist/profile-pointer-GRWJU3YD.js +31 -0
- package/dist/profile-select-74TRDEPD.js +23 -0
- package/dist/project-dependency-KSH63ODT.js +35 -0
- package/dist/project-sdk-REPPOIBD.js +24 -0
- package/dist/promote-command-PCWT5N6S.js +140 -0
- package/dist/prompt-II3F3LND.js +27 -0
- package/dist/publish-command-TTDUUJI5.js +414 -0
- package/dist/pull-command-VBO7C4S3.js +161 -0
- package/dist/pull-superseded-GJWLG7XO.js +20 -0
- package/dist/release-command-N4ZPKMHR.js +146 -0
- package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
- package/dist/response-HlXahv4K.d.ts +925 -0
- package/dist/retry-command-AS5EVREX.js +34 -0
- package/dist/routes-manifest-SNWUVBT6.js +43 -0
- package/dist/runtime-XETHVF33.js +14 -0
- package/dist/scaffold.d.ts +1911 -0
- package/dist/scaffold.js +107 -0
- package/dist/secrets-file-LVZD6IIM.js +56 -0
- package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
- package/dist/seed-VU7C3QYS.js +46 -0
- package/dist/sent-writes-N7X4OVXT.js +22 -0
- package/dist/source-resolve-S2XA3XZF.js +83 -0
- package/dist/static-host-B2Y6AG3W.js +352 -0
- package/dist/status-command-RUDXTF7I.js +554 -0
- package/dist/store-6VNCD4D7.js +20 -0
- package/dist/store-ARM6VMO5.js +98 -0
- package/dist/store-DuYfzKlW.d.ts +3144 -0
- package/dist/suggest-6FOOYKHG.js +12 -0
- package/dist/sveltekit.d.ts +154 -0
- package/dist/sveltekit.js +109 -0
- package/dist/sync-contributions-UCEROO26.js +415 -0
- package/dist/table-ENAVMITR.js +42 -0
- package/dist/tables-command-GUPXXSB5.js +156 -0
- package/dist/tenant-IIHPOHGP.js +42 -0
- package/dist/tenant-command-D5HGI2X4.js +965 -0
- package/dist/test-command-TIKZMDPZ.js +476 -0
- package/dist/tests-BGGIOK3L.js +26 -0
- package/dist/token-NICJUTMT.js +72 -0
- package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
- package/dist/toolchain-modules-5EAZUONB.js +24 -0
- package/dist/tracked-backend-EIBUMDZA.js +80 -0
- package/dist/update-check-2W2NLDTC.js +136 -0
- package/dist/upgrade-command-QHNFWM7O.js +531 -0
- package/dist/verify-rollout-DRCK5TDD.js +61 -0
- package/dist/whoami-command-STGINTXH.js +51 -0
- package/dist/workspace-2CLHU2LK.js +93 -0
- package/dist/workspace-binding-56PIIPCP.js +54 -0
- package/dist/workspace-command-JEXQETJD.js +535 -0
- package/dist/workspace-env-F2RFMSC5.js +70 -0
- package/dist/workspace-export-OJSH2HIW.js +21 -0
- package/dist/writable-VA7OISSK.js +25 -0
- package/dist/xano-ICFUSPFR.js +42 -0
- package/dist/xanosdk-import-GN42TO2B.js +56 -0
- package/dist/xanosdk-project-S6DXJONQ.js +41 -0
- package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
- package/guides/README.md +33 -0
- package/guides/authoring.md +921 -0
- package/guides/cli.md +401 -0
- package/guides/codegen.md +177 -0
- package/guides/codes.md +404 -0
- package/guides/coverage.md +74 -0
- package/guides/deploying.md +1525 -0
- package/guides/environment.md +400 -0
- package/guides/marketplace.md +415 -0
- package/guides/module-contract.md +558 -0
- package/guides/object-kinds.md +433 -0
- package/guides/project-structure.md +43 -0
- package/guides/scaffold.md +351 -0
- package/guides/typed-frontend.md +242 -0
- package/llms/client.md +63 -0
- package/llms/errors.md +56 -0
- package/llms/fields.md +176 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +30 -0
- package/llms/kinds-core.md +100 -0
- package/llms/kinds-knowledge.md +20 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +94 -0
- package/llms/legacy.md +44 -0
- package/llms/lock.md +40 -0
- package/llms/object-kinds.md +34 -0
- package/llms/statements-calls.md +62 -0
- package/llms/statements-catalog.md +320 -0
- package/llms/statements-data.md +66 -0
- package/llms/statements-runtime.md +68 -0
- package/llms/tests.md +59 -0
- package/llms/toolchain.md +16 -0
- package/llms/triggers.md +36 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +2085 -0
- package/llms.txt +493 -0
- package/manifest.json +11224 -0
- package/package.json +162 -0
- package/types/lambda-globals.d.ts +129 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Object kinds
|
|
2
|
+
|
|
3
|
+
Every object kind Xano SDK can author, and how to split a workspace across microservices.
|
|
4
|
+
|
|
5
|
+
## Object kinds
|
|
6
|
+
|
|
7
|
+
Every top-level Xano object is a registered kind with a factory and a `Xano.register*`
|
|
8
|
+
method: `defineFunction`, `table`, `query`, `apiGroup`, `tool`, `prompt`, `resource`,
|
|
9
|
+
`mcpServer`, `agent`,
|
|
10
|
+
`task`, `workflowTest`, `middleware`, `addon`, `realtimeServer`, `realtimeChannel`,
|
|
11
|
+
`realtimeMessage`, `knowledge`, `microservice` (its own section below), `workspaceConfig`,
|
|
12
|
+
and the seven trigger factories below. Signatures and payload keys are in
|
|
13
|
+
`llms/object-kinds.md` and `llms/triggers.md`; what follows is what
|
|
14
|
+
the types don't tell you.
|
|
15
|
+
|
|
16
|
+
**A knowledge item's body is a file, and `mode` is a running cost.** `knowledge()` is the
|
|
17
|
+
markdown a workspace's AI agents read before they act, and it is the one kind whose payload
|
|
18
|
+
is prose — so the body is named by path (`knowledgeFile("./runbook.md", import.meta.url)`)
|
|
19
|
+
rather than written as a string, and `refs: knowledgeDir(...)` ships a whole folder the
|
|
20
|
+
agent searches on demand — UTF-8 text files only (other content is refused), and a
|
|
21
|
+
symlink is not followed. What the agent actually receives is decided by `type` and `mode`,
|
|
22
|
+
and nothing in the types warns you: an `agents.md` item is injected in full on every turn
|
|
23
|
+
whatever `mode` says, `mode: "always"` spends the body's whole length on every request, and
|
|
24
|
+
the default `mode: "auto"` sends only the name and `description` until a request matches.
|
|
25
|
+
Write that `description` to be matched against a request rather than as a title. Full shape
|
|
26
|
+
in `llms/kinds-knowledge.md`.
|
|
27
|
+
|
|
28
|
+
**Triggers take a callback stack.** `stack: (t) => [...]` whenever the stack reads a trigger
|
|
29
|
+
input (a plain `stack: [...]`, as every other kind uses, is accepted when it reads none) —
|
|
30
|
+
because a trigger's inputs are **implied by its type** (fixed by Xano, not
|
|
31
|
+
editable) and injected automatically. So triggers take no `input` field, and the typed
|
|
32
|
+
handle `t` is the only way to read them (`response: (t) => ...` on response-bearing types).
|
|
33
|
+
The seven types are `tableTrigger`, `realtimeServerTrigger`, `realtimeChannelTrigger`,
|
|
34
|
+
`mcpServerTrigger`, `agentTrigger`, `workspaceTrigger`, and `errorTrigger`; they share one
|
|
35
|
+
stored envelope discriminated by `obj_type`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
tableTrigger({
|
|
39
|
+
name: "on-user-insert",
|
|
40
|
+
table: users,
|
|
41
|
+
actions: { insert: true },
|
|
42
|
+
// Optional row filter, evaluated by the DATABASE before the stack runs — so it
|
|
43
|
+
// names the SQL pseudo-tables with col(), NOT the t handle. Rejected with
|
|
44
|
+
// `truncate`; insert cannot read OLD.*, delete cannot read NEW.*.
|
|
45
|
+
search: cmp(col("NEW.email"), "!=", c.text("")),
|
|
46
|
+
stack: (t) => [
|
|
47
|
+
// t.new("email") is typed to the row; t.action is the op; t.old is null (insert-only).
|
|
48
|
+
s.db.add({ table: auditLog, row: { email: t.new("email"), event: t.action } }),
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**A workflow test is an end-to-end test, and its `datasource` decides what it can see.**
|
|
54
|
+
`workflowTest` takes no `input` and no `response` — it calls other objects and asserts on
|
|
55
|
+
what they bind. Leave `datasource` off by default: `""` runs against an **empty**
|
|
56
|
+
datasource. Naming one makes the engine **clone** that datasource before every run, so
|
|
57
|
+
pointing a test at production-sized data is slow enough to fail the run outright. `"live"`
|
|
58
|
+
warns at compile time; every other name is your call. On an **ephemeral**, `"live"` holds
|
|
59
|
+
only the seed fixtures, so it is how you read `table({ seed })` rows — a small clone,
|
|
60
|
+
dropped after the run. ⚠ The value is **stored on the object**, not a per-run flag: clear
|
|
61
|
+
it before you promote, or the same test clones the real database.
|
|
62
|
+
|
|
63
|
+
Empty means empty: **no `table({ seed })` rows exist while the test runs** by default, so
|
|
64
|
+
every `db` read misses unless the test specifies `datasource: "live"` (for seed data in
|
|
65
|
+
ephemerals) or creates what it needs first — typically a `defineFunction` fixture the stack
|
|
66
|
+
calls before anything else. A test written against a seeded row fails with your own
|
|
67
|
+
precondition message, which reads as a wrong id rather than an empty database. A failing
|
|
68
|
+
`s.api.call` is the other surprise: it **binds the error envelope** (`{code, message}`) to
|
|
69
|
+
its `as` and carries on rather than raising, so a later assertion gets blamed for a call
|
|
70
|
+
that failed several statements earlier — assert on `ref("r.code")` when a call may fail.
|
|
71
|
+
`llms/tests.md` carries the rest, including what `s.api.call` can and cannot do about
|
|
72
|
+
authentication.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
workflowTest({
|
|
76
|
+
name: "signup_works",
|
|
77
|
+
tags: ["smoke"],
|
|
78
|
+
// datasource omitted on purpose — "" is an EMPTY datasource, not "no datasource".
|
|
79
|
+
stack: [
|
|
80
|
+
s.function.call({ fn: createUser, input: { email: "a@b.c" }, as: "created" }),
|
|
81
|
+
s.expect.to_be_defined({ expr: ref("created") }),
|
|
82
|
+
s.expect.to_equal({ expr: ref("created.status"), value: c.text("ok") }),
|
|
83
|
+
// A regex assertion takes a PATTERN, so build it with `c.regex(...)`.
|
|
84
|
+
s.expect.to_match({ expr: ref("created.id"), value: c.regex("^usr_[a-z0-9]+$") }),
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Saved unit tests are a different thing, and they hang off the object.** A `query`,
|
|
90
|
+
`defineFunction` or `middleware` takes a `tests` array — the tests the Xano editor shows.
|
|
91
|
+
Each is a named set of inputs run against *that* object, asserted with the top-level
|
|
92
|
+
`expect.*` helpers. (`s.expect.*` — see [Authoring reference](authoring.md) — builds a
|
|
93
|
+
*statement* for a workflow-test stack;
|
|
94
|
+
`expect.*` builds a record stored on a test. They are not interchangeable, and the types
|
|
95
|
+
enforce it.) Any statement in the stack can return a **mock** instead of doing its work,
|
|
96
|
+
keyed by test name — and only while that named test runs, so a mock changes nothing about
|
|
97
|
+
a normal request.
|
|
98
|
+
|
|
99
|
+
A unit test's `datasource` works exactly as a workflow test's, with the same default:
|
|
100
|
+
`""` is an **empty** datasource, so **no `table({ seed })` rows are visible while a unit
|
|
101
|
+
test runs** either. Every `db` read misses, and an assertion on the first row fails against
|
|
102
|
+
a deployment whose endpoint returns those rows over HTTP a second later. The same
|
|
103
|
+
ephemeral-only `datasource: "live"` rule applies here, including clearing it before
|
|
104
|
+
promoting. Otherwise, create what the test needs inside the run — a `defineFunction`
|
|
105
|
+
fixture the stack calls first — or `mock` the read.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
query({
|
|
109
|
+
name: "score",
|
|
110
|
+
verb: "POST",
|
|
111
|
+
input: { score: input.int({ required: true }) },
|
|
112
|
+
tests: [
|
|
113
|
+
{
|
|
114
|
+
name: "adds one",
|
|
115
|
+
input: { score: c.int(1) },
|
|
116
|
+
// Subject first — argument order is the assertion.
|
|
117
|
+
expect: [expect.to_equal(resp(), c.int(2))],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
stack: [
|
|
121
|
+
// Returns 2 while "adds one" runs; does nothing on a real request. A name
|
|
122
|
+
// no test declares throws at compile time.
|
|
123
|
+
s.set_var("total", c.expression("$input.score + 1"), {
|
|
124
|
+
mock: { "adds one": c.int(2) },
|
|
125
|
+
}),
|
|
126
|
+
],
|
|
127
|
+
response: ref("total"),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A pull brings tests back, along with a query's saved request/response `example`. The one
|
|
132
|
+
thing it withholds is a test's auth `token` — that is an expiring credential rather than
|
|
133
|
+
authored configuration, so `xanosdk init --from` reports it as a deliberate omission instead of
|
|
134
|
+
writing it into a committed tree.
|
|
135
|
+
|
|
136
|
+
**Four of the `Run …` statements only run inside a workflow test.** `s.api.call`,
|
|
137
|
+
`s.task.call`, `s.trigger.call` and `s.workflow_test.call` are resolved by the engine at
|
|
138
|
+
run time, and outside a `workflowTest` stack it cannot reach the target — so one of them in
|
|
139
|
+
a query, function or task type-checks, exports, imports and **deploys clean**, then answers
|
|
140
|
+
the first real request
|
|
141
|
+
with `ERROR_FATAL: <Type> does not exist`. It is not per host kind: the same call fails
|
|
142
|
+
identically from a function that a query runs. `xanosdk export` refuses them outside a
|
|
143
|
+
workflow test. `s.function.call`, `s.tool.call`, `s.middleware.call` and `s.addon.call` run
|
|
144
|
+
from any stack, as does `s.function.run` — the ordinary way to invoke a function. To share
|
|
145
|
+
logic between two endpoints, put it in a `defineFunction` and `s.function.run` it from both.
|
|
146
|
+
|
|
147
|
+
**`s.expect.to_match` takes a regex PATTERN, not text.** The engine runs it through PHP
|
|
148
|
+
`preg_*`, which reads the first character as the delimiter — so a `c.text("^usr_.*$")` there
|
|
149
|
+
is a pattern the engine cannot run, and the assertion fails against the very string it was
|
|
150
|
+
written for. `c.regex("^usr_.*$")` (or `c.regex(/^usr_.*$/)`) wraps and escapes it; a bare
|
|
151
|
+
`c.text` pattern is refused at compile time and pointed here. A `ref`/`inp` pattern, whose
|
|
152
|
+
text isn't visible to the check, is passed through untouched.
|
|
153
|
+
|
|
154
|
+
**Realtime** — the only three-level containment chain in the SDK: `realtimeServer` owns
|
|
155
|
+
`realtimeChannel`s, which own `realtimeMessage` handlers (a message is the realtime
|
|
156
|
+
analogue of a query — its own typed payload and stack). Pass the **handle**, not a name: a
|
|
157
|
+
channel path is unique only within its server. A channel's `input` types its **path** params
|
|
158
|
+
(`rooms/{room_id}`); a message's `input` types the message **payload**. A server is off
|
|
159
|
+
until `enabled: true`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const chat = realtimeServer({ name: "chat", enabled: true });
|
|
163
|
+
|
|
164
|
+
const room = realtimeChannel({
|
|
165
|
+
name: "rooms/{room_id}", // `input` types the PATH params
|
|
166
|
+
server: chat,
|
|
167
|
+
input: { room_id: input.int() },
|
|
168
|
+
publish: { who: "authenticated" },
|
|
169
|
+
conversation: { enabled: true, limit: 50 }, // client-visible transcript
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
realtimeMessage({
|
|
173
|
+
name: "send", // `input` types the message PAYLOAD
|
|
174
|
+
channel: room, // the handle carries the server too
|
|
175
|
+
input: { body: input.text({ required: true }) },
|
|
176
|
+
deliverTo: "channel", // or "sender" (request/response) / "others"
|
|
177
|
+
stack: [s.debug.log({ value: inp("body") })],
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The client side is derived too, the same way `query().getPath()` works — `chat.getUrl(BASE)`
|
|
182
|
+
builds the socket URL (`wss://…/ws/<canonical>`, with a tenant base URL translated into the
|
|
183
|
+
socket's `/ws/<tenant>:<canonical>` form) and `room.getChannel({ room_id: 42 })` builds the
|
|
184
|
+
path a client joins. Both throw rather than guess. In a **browser bundle**, reach for the
|
|
185
|
+
generated manifest's `socketUrl`/`channelPath` instead — same addresses, same checks, without
|
|
186
|
+
importing the defs (see
|
|
187
|
+
[A type-safe frontend, for free](../README.md#a-type-safe-frontend-for-free)).
|
|
188
|
+
|
|
189
|
+
Five traps account for most realtime bugs. The full wire protocol — every server frame,
|
|
190
|
+
the presence roster shape, the at-least-once client contract — is in `llms/kinds-realtime.md`.
|
|
191
|
+
|
|
192
|
+
- **An empty return denies, and so does a crash.** `connect` and `join` are gates: return
|
|
193
|
+
`{ allowed: c.bool(true) }` or any truthy value to admit. A stack that falls through, or a gating
|
|
194
|
+
trigger with no `response`, refuses everyone — and a raise refuses too, because the gate is
|
|
195
|
+
seeded with a deny it keeps when the stack throws. Both failure modes lock the door, so the
|
|
196
|
+
risk to plan for is a self-inflicted lockout, not a breach: guard every drill inside a gate
|
|
197
|
+
with `ref(path, { safe: true })`, since `db.get` binds `null` on a miss. `export()` warns on
|
|
198
|
+
the missing `response`; nothing can warn about the raise. Gating is opt-in — a server with
|
|
199
|
+
no `connect` trigger admits everyone.
|
|
200
|
+
- **Only `null` drops a message.** In a `deliver` trigger (per recipient) and in a message
|
|
201
|
+
handler, `false`/`0`/`""` all deliver the message unchanged, and a crash broadcasts the
|
|
202
|
+
sender's original unvalidated payload. Return `null` to suppress. So a redaction check
|
|
203
|
+
written as a boolean sends the very message it was meant to hide. Per-viewer redaction
|
|
204
|
+
also takes **two objects**: the `deliver` trigger *and* `delivery: { perRecipient: true }`
|
|
205
|
+
on its channel. Either half alone delivers the payload unchanged to everyone, so a gate
|
|
206
|
+
whose return semantics are perfect still ships unredacted if the flag is missing — and the
|
|
207
|
+
flag costs a stack per recipient per message, so it is opt-in. `export()` warns on both
|
|
208
|
+
halves.
|
|
209
|
+
- **`conversation: { enabled: true }` alone stores nothing.** `limit` defaults to `0`, and
|
|
210
|
+
`0` means retain none. Always pass a `limit`. What a handler broadcasts *is* the stored
|
|
211
|
+
row, so broadcast everything a future joiner needs to render it.
|
|
212
|
+
- **An idle socket is reaped after ~10 minutes.** A listen-only client (feed, dashboard,
|
|
213
|
+
presence sidebar) must send `{ action: "ping" }` or any frame periodically, or it silently
|
|
214
|
+
drops and reconnects forever.
|
|
215
|
+
- **`s.realtime.publish` is the push direction, and it is fail-soft.** It bypasses the
|
|
216
|
+
channel's `publish.who` (authorization belongs in your stack), does not invoke the named
|
|
217
|
+
message's handler, and swallows a missing or disabled server — a mis-targeted publish is
|
|
218
|
+
silent. Pass the server handle and a filled-in path (`room.getChannel({ room_id: 42 })`),
|
|
219
|
+
never the template — a constant channel still carrying `{param}` throws at author time, and
|
|
220
|
+
a constant server or channel naming nothing this workspace registers warns at export.
|
|
221
|
+
|
|
222
|
+
**The superseded realtime layer.** Xano has had two realtime generations and they reuse the
|
|
223
|
+
same words. `realtimeTrigger(...)` and `s.api.realtime_event(...)` belong to the old
|
|
224
|
+
workspace-global layer; they are supported only so `codegen` can bring back a workspace that
|
|
225
|
+
holds them, and they are named in `llms/legacy.md` rather than in the authoring catalogs.
|
|
226
|
+
Aiming `s.api.realtime_event` at a current-layer channel publishes into the void — use
|
|
227
|
+
`s.realtime.publish({ server, channel, data })`, which names the owning server and so can
|
|
228
|
+
resolve the channel.
|
|
229
|
+
|
|
230
|
+
**MCP servers & agents** — both persist under the `toolset` payload key, so an `mcpServer`
|
|
231
|
+
and an `agent` **sharing a name collide**. A `tool({...})` is its own kind, referenced by
|
|
232
|
+
handle from either.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
// Auth is PER-TOOL and works like a query's: name an auth table({ auth: true }).
|
|
236
|
+
mcpServer({ name: "books", tools: [{ tool: searchTool, auth: users }] });
|
|
237
|
+
|
|
238
|
+
const assistant = agent({
|
|
239
|
+
name: "assistant",
|
|
240
|
+
llm: { type: "xano-free", systemPrompt: "Be helpful.", prompt: "Answer the question." },
|
|
241
|
+
// Pass the handles directly; the `{ tool, enabled?, auth? }` wrapper (above)
|
|
242
|
+
// is only for per-tool auth or `enabled: false`.
|
|
243
|
+
tools: [searchTool],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Agents have NO public endpoint — invoke them in-stack from any host with a stack.
|
|
247
|
+
query({
|
|
248
|
+
name: "ask", verb: "POST", apiGroup: api,
|
|
249
|
+
input: { question: input.text({ required: true }) },
|
|
250
|
+
stack: [s.ai.agent.run({ agent: assistant, args: obj({ question: inp("question") }), as: "answer" })],
|
|
251
|
+
response: { text: ref("answer.result") },
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
- **The run result is an envelope, not the completion.** The model's text is at **`.result`**
|
|
256
|
+
— `ref("answer")` is the whole metadata object (`finishReason`, `steps`, …). Both are
|
|
257
|
+
typed, so `InferResponse` reflects either.
|
|
258
|
+
- **`llm` is a provider-discriminated union** — `anthropic` / `openai` / `google-genai` /
|
|
259
|
+
`xano-free` (which needs no API key) — each with its provider's typed fields.
|
|
260
|
+
- **Structured output types the call site.** Author `output: { schema: { … } }` on the agent
|
|
261
|
+
with the `input.*` catalog and `.result` is typed from it wherever the handle is passed —
|
|
262
|
+
no second witness. The type-only `resultShape` is only for overriding that, or for an
|
|
263
|
+
agent referenced by bare name.
|
|
264
|
+
- **String settings are Twig-templated at run time.** The `args` you pass to
|
|
265
|
+
`s.ai.agent.run` become `{{ $args }}` (env vars are `{{ $env.NAME }}`), which is how an
|
|
266
|
+
endpoint's inputs reach the prompt. Numeric and boolean fields are not templated. Build a
|
|
267
|
+
dynamic arg with `obj({...})`, not `c.obj`.
|
|
268
|
+
- **`mcpServer().getUrl(HOST)`** derives the Streamable-HTTP endpoint from the def, the same
|
|
269
|
+
contract as `query.getPath()`. Resolve once — handing the result back in as a `HOST` throws
|
|
270
|
+
rather than append a second endpoint path. Agents expose only `getCanonical()`.
|
|
271
|
+
|
|
272
|
+
**MCP prompts and resources** are their own kinds (`registerPrompts` /
|
|
273
|
+
`registerResources`), exposed by listing them on an `mcpServer` — agents take tools only.
|
|
274
|
+
A tool, prompt or resource can also carry the metadata MCP clients show: a `title`,
|
|
275
|
+
`icons`, and on a tool, behavior `annotations` and an `output` schema.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
const summarize = prompt({
|
|
279
|
+
name: "summarize",
|
|
280
|
+
input: { post_id: input.int({ required: true }) },
|
|
281
|
+
stack: [s.db.get({ table: posts, fieldValue: inp("post_id"), as: "post" })],
|
|
282
|
+
response: ref("post.title"), // a string is one user message
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const post = resource({
|
|
286
|
+
name: "post",
|
|
287
|
+
uri: "posts://{post_id}", // each {variable} is exactly one input field
|
|
288
|
+
mimeType: "application/json",
|
|
289
|
+
input: { post_id: input.int({ required: true }) },
|
|
290
|
+
stack: [s.db.get({ table: posts, fieldValue: inp("post_id"), as: "post" })],
|
|
291
|
+
response: ref("post"),
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
mcpServer({ name: "posts", tools: [searchTool], prompts: [summarize], resources: [post] });
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- **`s.mcp.elicit` re-runs the stack.** It asks the client's user mid-call; when they
|
|
298
|
+
answer, the whole stack runs again from the top. Ask first, then act — `export()` warns
|
|
299
|
+
when a write comes before an elicit. Clients older than MCP `2026-07-28` get
|
|
300
|
+
`{ action: "cancel" }`, so handle it. `s.mcp.progress` reports progress to a client that
|
|
301
|
+
asked for it.
|
|
302
|
+
|
|
303
|
+
**Background execution.** `s.function.run` and `s.ai.agent.run` take a `runtime` block
|
|
304
|
+
(`{ mode: "async-shared" }`, or `"async-dedicated"` with `cpu`/`memory`/`timeout`/`maxRetry`)
|
|
305
|
+
that moves the call off the request path. This is **not** a performance knob: Xano rewrites
|
|
306
|
+
an async call to a statement that dispatches and continues, so it does not return the
|
|
307
|
+
function's result — don't bind `as` expecting a value. Collect results later with
|
|
308
|
+
`s.await({ ids })`.
|
|
309
|
+
|
|
310
|
+
## Microservices
|
|
311
|
+
|
|
312
|
+
A microservice is a container workload deployed alongside the workspace and called from a
|
|
313
|
+
stack with `s.microservice.request`. Two mutually exclusive shapes chosen by `kind`: `builtin`
|
|
314
|
+
declares containers (image/ports/resources/env/command/args) plus optional `ingresses`, and
|
|
315
|
+
`helm` points at a chart and its `values`; passing both throws.
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
export const echo = microservice({
|
|
319
|
+
name: "echo",
|
|
320
|
+
deployment: {
|
|
321
|
+
replicas: 2,
|
|
322
|
+
containers: [{
|
|
323
|
+
name: "echo",
|
|
324
|
+
image: "ealen/echo-server:latest",
|
|
325
|
+
ports: [{ servicePort: "8080", containerPort: "80" }],
|
|
326
|
+
resources: { cpu: "50m", ram: "256Mi" },
|
|
327
|
+
}],
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Call it by passing the def itself. `port` folds into the single `"name:port"` host string
|
|
333
|
+
the engine reads, and is optional — a microservice exposing exactly one `servicePort`
|
|
334
|
+
resolves to it, and one exposing several requires it. A port the microservice doesn't expose
|
|
335
|
+
is a type error where the def's ports are known, and a build-time throw otherwise:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
s.microservice.request({ as: "res", host: echo, path: "/health" });
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Only `host` and `path` are required. `method`, `params`, `headers`, `timeout`, and
|
|
342
|
+
`follow_location` default to the engine's own values (`GET`, `{}`, `[]`, `10`, `true`) and are
|
|
343
|
+
always written — this statement's schema requires them, so they can't be left off the wire;
|
|
344
|
+
you just don't have to type them.
|
|
345
|
+
|
|
346
|
+
`host` binds by name, not by guid, because that is how the engine resolves it — so renaming
|
|
347
|
+
a microservice fixes every call site at once. A plain `"name:port"` string is also accepted
|
|
348
|
+
and is the only way to reach an instance-level microservice, which isn't a workspace object;
|
|
349
|
+
nothing checks that spelling, so prefer the def wherever there is one.
|
|
350
|
+
|
|
351
|
+
A container takes time to come up, so `xanosdk deploy` waits for it: after the import it
|
|
352
|
+
reads each microservice and reports whether it is ready, still starting, or failed, then
|
|
353
|
+
lists them. Skip the wait with `--skip-liveness`. The same report is available any time from
|
|
354
|
+
`xanosdk status`, `xanosdk ephemeral get <name>`, and `xanosdk workspace details`.
|
|
355
|
+
|
|
356
|
+
Two outcomes, and only one of them is a warning:
|
|
357
|
+
|
|
358
|
+
- **The engine reports the microservice broken** (an image that won't pull, a container that
|
|
359
|
+
won't start) — the deploy **exits 4**. Waiting longer cannot change that answer, and a URL
|
|
360
|
+
and a ✓ printed over a dead workload is not a successful deploy. This is the default; there
|
|
361
|
+
is no flag to turn it off.
|
|
362
|
+
- **It simply hasn't reported ready by the end of the wait** — a warning, exit `0`. The
|
|
363
|
+
backend is live and a slow container usually follows moments later. Pass
|
|
364
|
+
**`--require-microservices`** to make that exit 4 too, which is what CI wants: nobody is
|
|
365
|
+
there to find out whether "should come up shortly" happened.
|
|
366
|
+
|
|
367
|
+
Exit 4 is the microservice sibling of exit 3 (a `--static` upload that failed while the
|
|
368
|
+
backend deploy stood): the import committed, and something it deployed is not serving. The
|
|
369
|
+
URL and the JSON summary still print either way — the exit code is what carries the
|
|
370
|
+
difference.
|
|
371
|
+
|
|
372
|
+
`tenantDeploy: "manual"` rows are reported but never waited on — nothing starts them for you.
|
|
373
|
+
Reach for it when the row should exist without a workload behind it; `examples/sandbox` uses
|
|
374
|
+
it so deploying the examples doesn't wait on containers.
|
|
375
|
+
|
|
376
|
+
Container names are free-form: they need not match the microservice's own name, and nothing
|
|
377
|
+
about addressing depends on them. A stack reaches the **microservice** name (plus a
|
|
378
|
+
`servicePort`), whichever containers sit behind it, so a multi-container workload names each
|
|
379
|
+
one for what it is.
|
|
380
|
+
|
|
381
|
+
**This surface is early and expected to change**, and every export of a workspace declaring a
|
|
382
|
+
microservice prints a notice saying so — the docs are read before writing, which is not where
|
|
383
|
+
you are when it matters. `configs` and `volumes` are typed and `@deprecated` but **not
|
|
384
|
+
deployable**: the engine rejects an import carrying either, so `export()` fails the build
|
|
385
|
+
rather than letting the deploy fatal minutes in, after provisioning has begun. Declare a value
|
|
386
|
+
the workload reads as a container `env` entry, and storage as a container `volumes` entry
|
|
387
|
+
(`emptyDir`, `persistent`, or `config`). Both fields stay typed so a pulled workspace holding
|
|
388
|
+
one still decodes.
|
|
389
|
+
|
|
390
|
+
Two fields carry secrets into the bundle — and into a pulled tree — verbatim:
|
|
391
|
+
`chart.values` and `registryAuth.dockerconfigjson`, because otherwise a pulled microservice
|
|
392
|
+
could not be redeployed.
|
|
393
|
+
|
|
394
|
+
**What "out of band" can and cannot mean here.** Reading `process.env` in a def is never it —
|
|
395
|
+
that resolves at EXPORT time, writing the literal into the bundle and into git with it:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// WRONG, for either field.
|
|
399
|
+
microservice({ name: "app", registryAuth: { dockerconfigjson: process.env.REGISTRY_JSON! } });
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
What the engine *does* resolve at deploy time is a **reference** to a workspace environment
|
|
403
|
+
variable (`workspaceConfig({ env })`). Two spellings, because a container env entry is a
|
|
404
|
+
structured slot and a chart value can sit at any path:
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
// A builtin container: name the variable, and the secret stays out of the bundle.
|
|
408
|
+
env: [{ name: "API_KEY", fromEnv: "STRIPE_SECRET" }]
|
|
409
|
+
|
|
410
|
+
// A helm chart: the same reference, for a value at any depth.
|
|
411
|
+
chart: { values: 'auth:\n token: "${env.STRIPE_SECRET}"\n' }
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Only the variable *name* travels in the bundle, rotating the workspace variable takes effect
|
|
415
|
+
on the next deploy with no edit here, and a reference the workspace does not define **fails
|
|
416
|
+
the deploy** rather than resolving blank. `value` and `fromEnv` are mutually exclusive on one
|
|
417
|
+
entry; setting both is a build error.
|
|
418
|
+
|
|
419
|
+
`registryAuth.dockerconfigjson` has **no** such form — it is not scanned for references — so
|
|
420
|
+
for that field there are two honest options, both about where the bytes live rather than about
|
|
421
|
+
hiding them:
|
|
422
|
+
|
|
423
|
+
1. **Leave `registryAuth` unset** — a public image, or a pull credential attached to the
|
|
424
|
+
microservice outside this workspace. Nothing then carries a credential.
|
|
425
|
+
2. **Accept that the tree is secret-bearing** — keep `workspace.json` and any pulled tree out
|
|
426
|
+
of git, or rotate the credential once it lands there.
|
|
427
|
+
|
|
428
|
+
Export prints a notice naming every microservice whose bundle bytes carry a literal (chart
|
|
429
|
+
values built entirely of `${env.NAME}` references carry nothing and are not reported), so
|
|
430
|
+
this can't happen quietly; `--strict` does **not** promote it, since shipping a
|
|
431
|
+
private-registry workload is a legitimate end state. When what you actually need is a secret
|
|
432
|
+
your *stack* reads, the mapped surface is `workspaceConfig({ env })` + `env("NAME")` — see
|
|
433
|
+
[Middleware, request history and env vars](authoring.md#middleware-request-history-and-env-vars).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Project structure
|
|
2
|
+
|
|
3
|
+
How a Xano SDK project is laid out on disk, and why registration is explicit.
|
|
4
|
+
|
|
5
|
+
Lay objects out however you like and register them explicitly — there's no folder
|
|
6
|
+
auto-discovery magic (deliberately):
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
xano/
|
|
10
|
+
├── function/ get_user.ts export const getUser = defineFunction({...})
|
|
11
|
+
├── table/ table.ts export const user = table({...})
|
|
12
|
+
│ └── trigger/ on_insert.ts export const onInsert = tableTrigger({...})
|
|
13
|
+
├── query/ public.ts export const publicApi = apiGroup({...})
|
|
14
|
+
│ public/posts_GET.ts export const posts = query({...})
|
|
15
|
+
├── agent/ assistant.ts export const assistant = agent({...})
|
|
16
|
+
├── realtime_server/ chat.ts export const chat = realtimeServer({...})
|
|
17
|
+
│ chat/room.ts export const room = realtimeChannel({...})
|
|
18
|
+
│ chat/room/send.ts export const send = realtimeMessage({...})
|
|
19
|
+
├── workspace.ts export const workspaceSettings = workspaceConfig({...})
|
|
20
|
+
└── index.ts workspace("my-app").registerTables([...]).registerFunctions([...])…
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Objects nest under whatever owns them. Anything with children — an API group, a
|
|
24
|
+
realtime server, a channel — is a file named for itself sitting *beside* the folder
|
|
25
|
+
holding its children, so `chat.ts` opens in a tab you can tell apart and a group with
|
|
26
|
+
no queries needs no folder at all. Realtime is the deepest, being the only three-level
|
|
27
|
+
hierarchy in a workspace — server, then channel, then message — and a trigger sits in
|
|
28
|
+
a `trigger/` folder at whichever level it fires on.
|
|
29
|
+
|
|
30
|
+
Paths are lower case throughout — an HTTP verb is the one exception, because it is
|
|
31
|
+
the method rather than a word. Bindings keep the object's own casing, so a file name
|
|
32
|
+
and the symbol it exports can differ.
|
|
33
|
+
|
|
34
|
+
That is the shape `xanosdk init --from` writes, and its `index.ts` re-exports every object
|
|
35
|
+
by name — import from the tree's root rather than from a file, since a file path moves
|
|
36
|
+
when an object's parent or its `_shared.ts` placement changes. Hand-authored projects are
|
|
37
|
+
free to use any other layout; only `index.ts` registering the objects matters.
|
|
38
|
+
|
|
39
|
+
`workspace("my-app")` is the natural entry point — sugar for
|
|
40
|
+
`new Xano().registerWorkspace({ name: "my-app" })`, returning the same chainable registry.
|
|
41
|
+
Authoring is **declarative def-objects** passed to factories; there is no callback/chaining
|
|
42
|
+
builder. `xano.export()` returns the importable `packageExport` bundle, and
|
|
43
|
+
`xanosdk export`/`deploy` read the module's default export.
|