okengine 0.18.4 → 0.19.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/AGENTS.md +18 -10
- package/README.md +2 -2
- package/package.json +51 -50
- package/site/content/docs/ai/index.mdx +25 -6
- package/site/content/docs/ai/llms-txt.mdx +1 -1
- package/site/content/docs/ai/meta.json +1 -1
- package/site/content/docs/ai/skills.mdx +4 -1
- package/site/content/docs/ai/try-it.mdx +58 -0
- package/site/content/docs/client/auth.mdx +208 -0
- package/site/content/docs/client/calling.mdx +451 -0
- package/site/content/docs/client/index.mdx +209 -0
- package/site/content/docs/client/live.mdx +234 -0
- package/site/content/docs/client/meta.json +5 -0
- package/site/content/docs/client/react.mdx +249 -0
- package/site/content/docs/elements/ai/agents.mdx +232 -0
- package/site/content/docs/elements/ai/index.mdx +341 -0
- package/site/content/docs/elements/ai/mcp.mdx +275 -0
- package/site/content/docs/elements/ai/meta.json +5 -0
- package/site/content/docs/elements/ai/models.mdx +283 -0
- package/site/content/docs/elements/ai/prompts.mdx +256 -0
- package/site/content/docs/elements/channel/email.mdx +266 -0
- package/site/content/docs/elements/channel/index.mdx +344 -0
- package/site/content/docs/elements/channel/meta.json +5 -0
- package/site/content/docs/elements/channel/push.mdx +218 -0
- package/site/content/docs/elements/channel/receipts.mdx +206 -0
- package/site/content/docs/elements/channel/sms.mdx +264 -0
- package/site/content/docs/elements/channel/whatsapp.mdx +220 -0
- package/site/content/docs/elements/clock/index.mdx +300 -0
- package/site/content/docs/elements/clock/meta.json +5 -0
- package/site/content/docs/elements/clock/schedules.mdx +508 -0
- package/site/content/docs/elements/clock/sleep.mdx +445 -0
- package/site/content/docs/elements/flow/consumers.mdx +707 -0
- package/site/content/docs/elements/flow/http.mdx +1080 -0
- package/site/content/docs/elements/flow/index.mdx +618 -0
- package/site/content/docs/elements/flow/meta.json +5 -0
- package/site/content/docs/elements/flow/routing.mdx +559 -0
- package/site/content/docs/elements/flow/workflows.mdx +640 -0
- package/site/content/docs/elements/gate/auth.mdx +371 -0
- package/site/content/docs/elements/gate/authorization.mdx +298 -0
- package/site/content/docs/elements/gate/index.mdx +418 -0
- package/site/content/docs/elements/gate/meta.json +5 -0
- package/site/content/docs/elements/gate/rate-limits.mdx +365 -0
- package/site/content/docs/elements/gate/rls.mdx +303 -0
- package/site/content/docs/elements/gate/tenancy.mdx +372 -0
- package/site/content/docs/elements/index.mdx +52 -11
- package/site/content/docs/elements/signal/broadcast.mdx +517 -0
- package/site/content/docs/elements/signal/index.mdx +352 -0
- package/site/content/docs/elements/signal/live.mdx +590 -0
- package/site/content/docs/elements/signal/meta.json +5 -0
- package/site/content/docs/elements/signal/once.mdx +596 -0
- package/site/content/docs/elements/store/files.mdx +659 -0
- package/site/content/docs/elements/store/index.mdx +310 -0
- package/site/content/docs/elements/store/kv.mdx +629 -0
- package/site/content/docs/elements/store/meta.json +5 -0
- package/site/content/docs/elements/store/search.mdx +946 -0
- package/site/content/docs/elements/store/sql.mdx +937 -0
- package/site/content/docs/elements/vault/config.mdx +256 -0
- package/site/content/docs/elements/vault/index.mdx +356 -0
- package/site/content/docs/elements/vault/meta.json +5 -0
- package/site/content/docs/elements/vault/rotation.mdx +273 -0
- package/site/content/docs/elements/vault/secrets.mdx +285 -0
- package/site/content/docs/index.mdx +78 -30
- package/site/content/docs/meta.json +11 -8
- package/site/content/docs/plugins/cors.mdx +2 -1
- package/site/content/docs/plugins/csrf.mdx +4 -1
- package/site/content/docs/plugins/headers.mdx +2 -1
- package/site/content/docs/plugins/ip-allowlist.mdx +2 -1
- package/site/content/docs/plugins/magic-link.mdx +6 -0
- package/site/content/docs/plugins/maintenance-mode.mdx +2 -1
- package/site/content/docs/plugins/oauth.mdx +1 -1
- package/site/content/docs/plugins/otp.mdx +12 -1
- package/site/content/docs/plugins/passkey.mdx +39 -15
- package/site/content/docs/plugins/two-factor.mdx +91 -34
- package/site/content/docs/plugins/username.mdx +1 -1
- package/site/content/docs/recipes/caddy.mdx +3 -3
- package/site/content/docs/recipes/index.mdx +18 -28
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +3 -3
- package/site/content/docs/recipes/meta.json +1 -4
- package/site/content/docs/recipes/nginx.mdx +2 -2
- package/site/content/docs/recipes/openrouter.mdx +283 -0
- package/site/content/docs/recipes/pgdog.mdx +3 -3
- package/site/content/docs/recipes/postgres.mdx +1 -2
- package/site/content/docs/recipes/rustfs.mdx +2 -2
- package/site/content/docs/recipes/traefik.mdx +3 -3
- package/site/content/docs/reference/cli.mdx +231 -0
- package/site/content/docs/reference/configuration.mdx +30 -10
- package/site/content/docs/reference/environment-variables.mdx +93 -20
- package/site/content/docs/reference/errors.mdx +132 -29
- package/site/content/docs/reference/fx.mdx +137 -73
- package/site/content/docs/reference/index.mdx +13 -21
- package/site/content/docs/reference/meta.json +4 -5
- package/site/content/docs/reference/okid.mdx +42 -17
- package/site/content/docs/reference/plugins.mdx +4 -3
- package/site/content/docs/reference/security.mdx +197 -0
- package/site/content/docs/understand/meta.json +5 -0
- package/site/content/docs/understand/the-anatomy.mdx +132 -0
- package/site/content/docs/understand/the-model.mdx +32 -0
- package/site/content/docs/understand/the-problem.mdx +74 -0
- package/site/content/docs/understand/the-vocabulary.mdx +26 -0
- package/src/auth/api-keys.ts +2 -1
- package/src/auth/bindings.ts +64 -16
- package/src/auth/gate-auth.test.ts +6 -1
- package/src/auth/identity.ts +152 -5
- package/src/auth/index.ts +29 -0
- package/src/auth/invites.ts +2 -1
- package/src/auth/method-context.ts +41 -0
- package/src/auth/oauth-as/crypto.ts +2 -1
- package/src/auth/oauth-as/stores.ts +2 -1
- package/src/auth/operator.ts +2 -1
- package/src/auth/sessions-jwt.test.ts +77 -0
- package/src/auth/sessions.ts +20 -4
- package/src/auth/tenants.ts +3 -2
- package/src/auth/two-factor-challenge.test.ts +61 -0
- package/src/auth/two-factor-challenge.ts +284 -0
- package/src/auth/verification.ts +5 -0
- package/src/bench/REPORT.md +49 -0
- package/src/bench/g03-signal-once.bench.ts +1 -1
- package/src/bench/g10-observability-contention.bench.ts +1 -1
- package/src/bench/g17-hybrid-search.bench.ts +572 -0
- package/src/bench/load-app.ts +2 -10
- package/src/cli/ai-setup/ai-setup.test.ts +331 -42
- package/src/cli/ai-setup/apply.ts +294 -52
- package/src/cli/ai-setup/catalog.ts +238 -1342
- package/src/cli/ai-setup/index.ts +14 -99
- package/src/cli/ai-setup/prompts.ts +60 -443
- package/src/cli/ask-seed.test.ts +96 -0
- package/src/cli/ask-seed.ts +82 -0
- package/src/cli/ask-vault-gaps.test.ts +84 -0
- package/src/cli/ask-vault-gaps.ts +146 -0
- package/src/cli/build.ts +2 -2
- package/src/cli/client-add.test.ts +26 -1
- package/src/cli/client-add.ts +106 -27
- package/src/cli/competitor-mention-removal.test.ts +1 -1
- package/src/cli/db-seed.ts +2 -0
- package/src/cli/db.ts +150 -11
- package/src/cli/dev.ts +119 -211
- package/src/cli/docker-clean.ts +2 -2
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/doctor-diff.ts +4 -2
- package/src/cli/doctor-pii.test.ts +1 -1
- package/src/cli/doctor.ts +14 -1
- package/src/cli/hero-meta.test.ts +4 -2
- package/src/cli/load-config.images.test.ts +8 -10
- package/src/cli/load-config.ts +1 -1
- package/src/cli/registry.ts +22 -10
- package/src/cli/replay.ts +3 -1
- package/src/cli/start.ts +1 -1
- package/src/cli/tui/keys.ts +2 -3
- package/src/client/auth/cookies.ts +74 -0
- package/src/client/auth/create-auth-client.ts +636 -0
- package/src/client/auth/denials.ts +99 -0
- package/src/client/auth/session.ts +283 -0
- package/src/client/auth.test.ts +251 -0
- package/src/client/auth.ts +39 -114
- package/src/client/create-with-session.ts +245 -0
- package/src/client/create.ts +51 -5
- package/src/client/index.ts +11 -1
- package/src/client/live.ts +11 -79
- package/src/client/notes-contract.test.ts +5 -10
- package/src/client/sse.ts +137 -0
- package/src/client/stream.ts +147 -0
- package/src/client/transport.test.ts +32 -0
- package/src/client/transport.ts +123 -26
- package/src/client/types.ts +118 -14
- package/src/client-react/index.ts +185 -24
- package/src/client-react/use-live-query.ts +13 -3
- package/src/compiler/aot.test.ts +7 -4
- package/src/compiler/effects-embed.test.ts +56 -0
- package/src/compiler/effects-fetch.test.ts +43 -0
- package/src/compiler/effects-infer.ts +58 -2
- package/src/compiler/extract.test.ts +258 -44
- package/src/compiler/extract.ts +566 -48
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +7 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/signals.ts +2 -8
- package/src/compiler/fixtures/skyport.expected.json +4 -4
- package/src/compiler/fixtures/triggers/five-triggers.ts +14 -10
- package/src/compiler/response.ts +2 -2
- package/src/compiler/search-writer-isolation.test.ts +40 -0
- package/src/config/index.ts +11 -0
- package/src/console/server/app.ts +17 -23
- package/src/console/server/bind.ts +4 -0
- package/src/console/server/console.test.ts +2 -2
- package/src/console/server/flows-invoke.test.ts +50 -34
- package/src/console/server/flows.ts +436 -255
- package/src/console/server/runs-ingest.test.ts +6 -3
- package/src/console/server/serve.ts +1 -1
- package/src/console/server/signals.test.ts +1 -5
- package/src/console/server/signals.ts +8 -6
- package/src/console/server/state.ts +6 -1
- package/src/console/server/store.ts +1 -1
- package/src/console/ui-next/dist/assets/FileExportIcon-Ck-5od4R.js +1 -0
- package/src/console/ui-next/dist/assets/MoreHorizontalCircle01Icon-gMNGsE37.js +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +1 -0
- package/src/console/ui-next/dist/assets/UnavailableIcon-D9cvHVPr.js +1 -0
- package/src/console/ui-next/dist/assets/UserIcon-DaE2PB5_.js +1 -0
- package/src/console/ui-next/dist/assets/access-page-DFeymU07.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-U1rdfblp.js +1 -0
- package/src/console/ui-next/dist/assets/cache-glyph-BeFJeqBG.js +1 -0
- package/src/console/ui-next/dist/assets/call-pii-button-CVAONPii.js +1 -0
- package/src/console/ui-next/dist/assets/collapsible-D2A6NJ-3.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +1 -0
- package/src/console/ui-next/dist/assets/dagre.esm-B1_XeuLP.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Cgk_h5ja.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-Dss7941e.js +1 -0
- package/src/console/ui-next/dist/assets/highlighted-json-MYZQtRnw.js +154 -0
- package/src/console/ui-next/dist/assets/http-method-Jrh39p7A.js +1 -0
- package/src/console/ui-next/dist/assets/index-D0zS5rKO.css +2 -0
- package/src/console/ui-next/dist/assets/index-DXP2dBIF.js +63 -0
- package/src/console/ui-next/dist/assets/observability-page-HvolXxTI.js +4 -0
- package/src/console/ui-next/dist/assets/react-dom-Ddte4I-Q.js +9 -0
- package/src/console/ui-next/dist/assets/replica-lag-CSh2dzrb.js +18 -0
- package/src/console/ui-next/dist/assets/request-meta-BatF8KrK.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +1 -0
- package/src/console/ui-next/dist/assets/{sql-BskegiFM.js → sql-BsFa4tDR.js} +1 -1
- package/src/console/ui-next/dist/assets/store-page-eiKiHnNe.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CZkMeKS-.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CW8y5A2h.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-B_RWJrEO.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-uk4WVboC.js → use-vault-list-CT4-gajj.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-CWrg-A68.js +2 -0
- package/src/console/ui-next/dist/assets/xyflow-CSyC6ryz.css +1 -0
- package/src/console/ui-next/dist/assets/xyflow-yApv7D4e.js +7 -0
- package/src/console/ui-next/dist/index.html +6 -13
- package/src/console/ui-next/seed-invoke-host.ts +21 -18
- package/src/console/ui-next/src/client.ts +7 -2
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +2 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +12 -2
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +8 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-bars.ts +4 -0
- package/src/console/ui-next/src/features/flows/traces/waterfall-tooltip.ts +9 -3
- package/src/console/ui-next/src/features/store/detail/reveal-cell.tsx +7 -11
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +2 -2
- package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +2 -0
- package/src/console/ui-next/src/features/store/lib/grid-model.test.ts +25 -1
- package/src/console/ui-next/src/features/store/lib/grid-model.ts +36 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +3 -3
- package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +2 -0
- package/src/console/ui-next/src/features/units/lib/call-read-safe.ts +3 -1
- package/src/console/ui-next/src/features/units/lib/fields-from-schema.ts +6 -0
- package/src/docker/ai-model-status.test.ts +1 -12
- package/src/docker/ai-model-status.ts +10 -38
- package/src/docker/compose-up.test.ts +100 -0
- package/src/docker/compose-up.ts +292 -0
- package/src/docker/compose.ts +4 -14
- package/src/docker/derive.ts +9 -34
- package/src/docker/docker.test.ts +16 -261
- package/src/docker/images-config.test.ts +13 -25
- package/src/docker/index.ts +1 -24
- package/src/docker/recipes/index.ts +0 -23
- package/src/docker/recipes/pgdog.ts +6 -4
- package/src/docker/stack-id.test.ts +2 -2
- package/src/docker/stack-id.ts +0 -1
- package/src/docker/types.ts +2 -2
- package/src/drivers/ai-anthropic.ts +10 -0
- package/src/drivers/ai-openai-compatible.ts +34 -7
- package/src/drivers/ai-providers.test.ts +0 -107
- package/src/drivers/ai-stream.test.ts +0 -34
- package/src/drivers/ai-types.ts +17 -8
- package/src/drivers/channel-fcm.ts +6 -1
- package/src/drivers/channel-msegat.ts +6 -1
- package/src/drivers/channel-resend.ts +5 -1
- package/src/drivers/channel-smtp.ts +5 -1
- package/src/drivers/channel-sndr.ts +10 -1
- package/src/drivers/channel-taqnyat-mail.ts +5 -1
- package/src/drivers/channel-taqnyat-whatsapp.ts +6 -1
- package/src/drivers/channel-taqnyat.ts +6 -1
- package/src/drivers/channel-types.ts +10 -0
- package/src/drivers/channel-unifonic.ts +6 -1
- package/src/drivers/channel-wa-cloud.ts +6 -1
- package/src/drivers/channel-webpush.ts +6 -1
- package/src/drivers/external.ts +33 -0
- package/src/drivers/index.ts +0 -10
- package/src/drivers/meilisearch.ts +6 -0
- package/src/drivers/oauth-types.ts +4 -0
- package/src/drivers/postgres.test.ts +74 -3
- package/src/drivers/postgres.ts +175 -6
- package/src/drivers/signal-types.ts +5 -5
- package/src/drivers/types.ts +7 -0
- package/src/drivers/vault-types.ts +5 -0
- package/src/elements/ai/declare.ts +17 -4
- package/src/elements/ai/eval.ts +1 -1
- package/src/elements/ai/mcp-http.ts +1 -1
- package/src/elements/ai/pii.ts +1 -1
- package/src/elements/ai/providers.test.ts +289 -0
- package/src/elements/ai/providers.ts +169 -0
- package/src/elements/ai/runtime.ts +68 -5
- package/src/elements/ai/schema.ts +2 -2
- package/src/elements/ai.test.ts +7 -7
- package/src/elements/ai.ts +17 -1
- package/src/elements/channel/runtime.ts +105 -12
- package/src/elements/clock/cron-fields.test.ts +144 -0
- package/src/elements/clock/cron-fields.ts +185 -0
- package/src/elements/clock/declare.ts +247 -3
- package/src/elements/clock.ts +21 -1
- package/src/elements/index.ts +15 -0
- package/src/elements/signal/chaos-child.ts +1 -2
- package/src/elements/signal/declare.ts +65 -37
- package/src/elements/signal/delivery-modes.test.ts +11 -31
- package/src/elements/signal/dry-run-replay.test.ts +1 -7
- package/src/elements/signal/dry-run-write-isolation.test.ts +1 -7
- package/src/elements/signal/key-ordering.test.ts +6 -26
- package/src/elements/signal/lease-reclaim.test.ts +2 -6
- package/src/elements/signal/optional-emit.test.ts +6 -9
- package/src/elements/signal/order-lifecycle.test.ts +4 -17
- package/src/elements/signal/orphan-messages.test.ts +4 -15
- package/src/elements/signal/reconcile.test.ts +2 -2
- package/src/elements/signal/runtime.ts +1 -1
- package/src/elements/signal/schema-emit.test.ts +4 -8
- package/src/elements/signal.test.ts +24 -49
- package/src/elements/signal.ts +9 -2
- package/src/elements/store/domain-ddl.test.ts +1 -1
- package/src/elements/store/live-http.test.ts +9 -4
- package/src/elements/store/prepare-row.test.ts +123 -0
- package/src/elements/store/resource.ts +43 -12
- package/src/elements/store/schema-decl.ts +117 -0
- package/src/elements/store/search-backfill.ts +205 -0
- package/src/elements/store/search-bind.ts +66 -0
- package/src/elements/store/search-bm25.ts +78 -0
- package/src/elements/store/search-ddl.ts +156 -0
- package/src/elements/store/search-embed-flow.ts +141 -0
- package/src/elements/store/search-errors.ts +34 -0
- package/src/elements/store/search-fusion.ts +112 -0
- package/src/elements/store/search-lsh.ts +179 -0
- package/src/elements/store/search-runtime.ts +300 -0
- package/src/elements/store/search.test.ts +144 -0
- package/src/elements/store/sql-session.test.ts +1 -0
- package/src/elements/store/sql-session.ts +83 -3
- package/src/elements/store/table.ts +23 -2
- package/src/elements/store/upsert-app.test.ts +1 -3
- package/src/elements/store.ts +50 -0
- package/src/full.ts +4 -2
- package/src/http.ts +4 -1
- package/src/i18n/catalogs/ar.ts +14 -6
- package/src/i18n/catalogs/en.ts +14 -6
- package/src/index.ts +6 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +7 -7
- package/src/kernel/adopt-routes.ts +9 -1
- package/src/kernel/app-auth.ts +5 -0
- package/src/kernel/app.ts +102 -9
- package/src/kernel/auto-cache.test.ts +6 -12
- package/src/kernel/auto-registry.test.ts +3 -3
- package/src/kernel/boot-bind/ai.test.ts +22 -19
- package/src/kernel/boot-bind/ai.ts +5 -43
- package/src/kernel/boot-bind/clock.ts +23 -5
- package/src/kernel/boot-bind/honor-config.test.ts +4 -4
- package/src/kernel/boot.test.ts +11 -6
- package/src/kernel/boot.ts +66 -16
- package/src/kernel/boundary-contract.ts +93 -0
- package/src/kernel/budget.test.ts +1 -1
- package/src/kernel/call.ts +89 -0
- package/src/kernel/capability.ts +6 -0
- package/src/kernel/client-descriptor.ts +123 -0
- package/src/kernel/clock-timezone.test.ts +80 -0
- package/src/kernel/correlation.test.ts +1 -1
- package/src/kernel/dry-run.ts +11 -8
- package/src/kernel/effects-stamping.test.ts +42 -9
- package/src/kernel/effects.test.ts +4 -2
- package/src/kernel/effects.ts +51 -9
- package/src/kernel/errors-channel.ts +17 -0
- package/src/kernel/errors-live-resume.ts +3 -2
- package/src/kernel/errors-tenant.ts +7 -4
- package/src/kernel/errors.registry-helpers.ts +96 -0
- package/src/kernel/errors.registry.test.ts +139 -25
- package/src/kernel/errors.ts +102 -23
- package/src/kernel/external-effects.test.ts +196 -0
- package/src/kernel/flow.test.ts +13 -4
- package/src/kernel/flow.ts +69 -71
- package/src/kernel/fx-ask-telemetry.test.ts +2 -2
- package/src/kernel/fx-dead-letters.test.ts +3 -3
- package/src/kernel/fx-fetch.ts +55 -0
- package/src/kernel/fx-live-stream.ts +2 -2
- package/src/kernel/fx-live.test.ts +8 -8
- package/src/kernel/fx.test.ts +17 -11
- package/src/kernel/fx.ts +180 -57
- package/src/kernel/horizontal-child.ts +1 -1
- package/src/kernel/http-query.test.ts +3 -6
- package/src/kernel/index.ts +13 -3
- package/src/kernel/instance-id.ts +1 -1
- package/src/kernel/instances.test.ts +40 -0
- package/src/kernel/instances.ts +47 -9
- package/src/kernel/live-http.test.ts +6 -7
- package/src/kernel/live-http.ts +6 -2
- package/src/kernel/live-resume.test.ts +1 -1
- package/src/kernel/mcp-tool.test.ts +3 -3
- package/src/kernel/on.ts +98 -4
- package/src/kernel/pipeline.test.ts +12 -15
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/stamp-http.test.ts +3 -3
- package/src/kernel/triggers.ts +210 -95
- package/src/kernel-entry.ts +0 -1
- package/src/manifest/diff.ts +11 -1
- package/src/manifest/fixtures/skyport.excerpt.json +1 -1
- package/src/manifest/types.ts +46 -2
- package/src/mcp/docs-index.ts +3 -3
- package/src/mcp/docs-mcp.test.ts +2 -2
- package/src/mcp/docs-tools.ts +1 -1
- package/src/okid.test.ts +56 -0
- package/src/okid.ts +55 -11
- package/src/plugins/anonymous.ts +8 -4
- package/src/plugins/auth/shared.ts +27 -3
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/auth-methods.security.test.ts +561 -23
- package/src/plugins/config-source.ts +1 -1
- package/src/plugins/csrf.test.ts +65 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/magic-link.ts +36 -16
- package/src/plugins/oauth/shared.ts +3 -1
- package/src/plugins/oauth.ts +22 -14
- package/src/plugins/otp.ts +37 -21
- package/src/plugins/passkey-webauthn.ts +9 -6
- package/src/plugins/passkey.ts +98 -26
- package/src/plugins/pre-account-hijack.test.ts +223 -0
- package/src/plugins/two-factor.ts +466 -37
- package/src/plugins/username.ts +19 -7
- package/src/release/build-lib.ts +1 -0
- package/src/release/limits.ts +2 -2
- package/src/release/measure.ts +1 -1
- package/src/runs/collect.test.ts +0 -2
- package/src/runtime/json-code-block.test.ts +250 -20
- package/src/runtime/json-code-block.ts +1261 -41
- package/src/term.test.ts +1 -1
- package/src/test/create-test-app.test.ts +12 -12
- package/src/test/live-signals.test.ts +6 -6
- package/src/test/provisions.integration.test.ts +10 -14
- package/src/test/reset-element-registries.ts +1 -1
- package/src/test/tenant-isolation.test.ts +12 -6
- package/site/content/docs/deployment/docker-swarm.mdx +0 -164
- package/site/content/docs/deployment/docker.mdx +0 -227
- package/site/content/docs/deployment/index.mdx +0 -83
- package/site/content/docs/deployment/kubernetes.mdx +0 -176
- package/site/content/docs/deployment/meta.json +0 -5
- package/site/content/docs/deployment/reverse-proxy.mdx +0 -234
- package/site/content/docs/elements/ai.mdx +0 -385
- package/site/content/docs/elements/channel.mdx +0 -346
- package/site/content/docs/elements/clock.mdx +0 -244
- package/site/content/docs/elements/flow.mdx +0 -420
- package/site/content/docs/elements/gate.mdx +0 -436
- package/site/content/docs/elements/signal.mdx +0 -380
- package/site/content/docs/elements/store.mdx +0 -1099
- package/site/content/docs/elements/vault.mdx +0 -405
- package/site/content/docs/get-started/basic-usage.mdx +0 -173
- package/site/content/docs/get-started/index.mdx +0 -43
- package/site/content/docs/get-started/installation.mdx +0 -220
- package/site/content/docs/get-started/introduction.mdx +0 -132
- package/site/content/docs/get-started/meta.json +0 -13
- package/site/content/docs/get-started/project-structure.mdx +0 -925
- package/site/content/docs/get-started/testing.mdx +0 -328
- package/site/content/docs/get-started/why.mdx +0 -162
- package/site/content/docs/recipes/llama-cpp.mdx +0 -151
- package/site/content/docs/recipes/ollama.mdx +0 -142
- package/site/content/docs/recipes/sglang.mdx +0 -105
- package/site/content/docs/recipes/vllm.mdx +0 -106
- package/site/content/docs/reference/cli.md +0 -232
- package/site/content/docs/reference/client.mdx +0 -469
- package/site/content/docs/reference/security.md +0 -74
- package/src/cli/ai-setup/detect-ollama.ts +0 -213
- package/src/cli/ai-setup/recommend.test.ts +0 -225
- package/src/cli/ai-setup/recommend.ts +0 -225
- package/src/cli/dev-controls.test.ts +0 -122
- package/src/cli/dev-controls.ts +0 -164
- package/src/cli/tui/DevLive.tsx +0 -126
- package/src/console/ui-next/dist/assets/access-page-De7Lc2JC.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-BP0Y0Sux.js +0 -1
- package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +0 -1
- package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +0 -1
- package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +0 -1
- package/src/console/ui-next/dist/assets/dagre.esm-ZwcdTuZZ.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-sC3lGABz.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-RGy7VEA_.js +0 -1
- package/src/console/ui-next/dist/assets/highlighted-json-Awq7gYdu.js +0 -154
- package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +0 -2
- package/src/console/ui-next/dist/assets/index-_rgpdVzo.js +0 -66
- package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +0 -1
- package/src/console/ui-next/dist/assets/observability-page-Ds6pcnh-.js +0 -4
- package/src/console/ui-next/dist/assets/preload-helper-oH4irX4C.js +0 -1
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +0 -1
- package/src/console/ui-next/dist/assets/react-dom-Bph1y7z7.js +0 -9
- package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +0 -18
- package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-02xOiqIK.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-4rHOePuE.js +0 -1
- package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +0 -1
- package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DISPgxLM.js +0 -2
- package/src/console/ui-next/dist/assets/xyflow-D7n4g6go.js +0 -7
- package/src/console/ui-next/dist/assets/xyflow-DZ0Ws1xk.css +0 -1
- package/src/docker/ollama-pull.ts +0 -232
- package/src/docker/recipes/llama-cpp.ts +0 -298
- package/src/docker/recipes/ollama.ts +0 -44
- package/src/docker/recipes/sglang.ts +0 -55
- package/src/docker/recipes/vllm.ts +0 -44
- package/src/drivers/ai-ollama-tools.integration.test.ts +0 -109
- package/src/drivers/ai-ollama.integration.test.ts +0 -184
- package/src/drivers/ai-ollama.ts +0 -389
- package/src/drivers/ollama.ts +0 -14
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "SQL"
|
|
3
|
+
description: "Relational schemas, field classification, store.resource CRUD, RLS policies, and the fx.store session handle."
|
|
4
|
+
icon: "Table"
|
|
5
|
+
source: "docs/spec/unified-theory.md"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The SQL facet is how you declare relational tables and read or write them from Flows. Schemas use `store.schema.table` + `field.*`; I/O goes through `fx.store(db)`.
|
|
9
|
+
|
|
10
|
+
For developers modeling domain data on okengine — mark columns, push schema, query with the session handle (or mount `store.resource`).
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Declare tables with `store.schema.table`. Touch them only via `fx.store(db)` — `select` / `insert`
|
|
14
|
+
/ `update` / `delete` / `page` / `search`. There is no `fx.store(db).query.…` Relational Query
|
|
15
|
+
Builder on the handle.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Smallest Example
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Define a table and bind the store
|
|
24
|
+
|
|
25
|
+
```typescript title="src/db/schema.decl.ts"
|
|
26
|
+
import { store, field } from "okengine";
|
|
27
|
+
|
|
28
|
+
export const posts = store.schema.table("posts", {
|
|
29
|
+
id: field.id().primaryKey(),
|
|
30
|
+
title: field.text().notNull(),
|
|
31
|
+
authorId: field.text().notNull(),
|
|
32
|
+
publishedAt: field.timestamp(),
|
|
33
|
+
createdAt: field.timestamp().notNull().now(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const db = store.sql("app", { schema: { posts } });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
</Step>
|
|
40
|
+
|
|
41
|
+
<Step>
|
|
42
|
+
### Query in a Flow
|
|
43
|
+
|
|
44
|
+
```typescript title="src/flows/posts/list.ts"
|
|
45
|
+
import { on, flow, http } from "okengine";
|
|
46
|
+
import { db, posts } from "@/schema";
|
|
47
|
+
|
|
48
|
+
export const list = on(
|
|
49
|
+
http.get(),
|
|
50
|
+
flow({
|
|
51
|
+
do: async (_, fx) => {
|
|
52
|
+
return await fx.store(db).page(posts, {
|
|
53
|
+
orderBy: [posts.createdAt],
|
|
54
|
+
limit: 20,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
</Step>
|
|
62
|
+
|
|
63
|
+
<Step>
|
|
64
|
+
### Push schema and call
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
oke db push
|
|
68
|
+
curl -X GET http://localhost:6530/posts -H "accept: application/json"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Response:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"data": [{ "id": "…", "title": "…", "authorId": "…", "publishedAt": null, "createdAt": "…" }],
|
|
76
|
+
"error": null
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
</Step>
|
|
81
|
+
|
|
82
|
+
</Steps>
|
|
83
|
+
|
|
84
|
+
<Callout title="Pathless declare">
|
|
85
|
+
Starters often pass the whole decl module: `store.sql("app", {schema})` where `schema` is `import
|
|
86
|
+
* as schema from "@/db/schema.decl"`. Named keys and the exported table handles are equivalent.
|
|
87
|
+
</Callout>
|
|
88
|
+
|
|
89
|
+
## Progressive Patterns
|
|
90
|
+
|
|
91
|
+
Explore SQL from a bare table to classified columns, CRUD factory, and RLS:
|
|
92
|
+
|
|
93
|
+
<Tabs items={["Schema", "Classify", "Resource", "RLS"]}>
|
|
94
|
+
|
|
95
|
+
<Tab value="Schema">
|
|
96
|
+
|
|
97
|
+
`field.id()` is `text` + auto id on insert. Use `.okid()` on an existing text
|
|
98
|
+
column when you want the same default without the sugar factory:
|
|
99
|
+
|
|
100
|
+
```typescript title="src/db/schema.decl.ts"
|
|
101
|
+
import { store, field } from "okengine";
|
|
102
|
+
|
|
103
|
+
export const notes = store.schema.table("notes", {
|
|
104
|
+
id: field.id().primaryKey(),
|
|
105
|
+
title: field.text().notNull(),
|
|
106
|
+
body: field.text().notNull(),
|
|
107
|
+
archivedAt: field.timestamp(),
|
|
108
|
+
createdAt: field.timestamp().notNull().now(),
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
</Tab>
|
|
113
|
+
|
|
114
|
+
<Tab value="Classify">
|
|
115
|
+
|
|
116
|
+
`.pii()` / `.sensitive()` / `.retain(duration)` tag columns for posture audits
|
|
117
|
+
and AI egress masking:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
export const users = store.schema.table("users", {
|
|
121
|
+
id: field.id().primaryKey(),
|
|
122
|
+
email: field.text().unique().pii().notNull(),
|
|
123
|
+
passwordHash: field.text().sensitive().notNull(),
|
|
124
|
+
lastLoginAt: field.timestamp().retain("90d"),
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Consequence:** AI prompts and Console redaction read these tags — they are not
|
|
129
|
+
decorative comments.
|
|
130
|
+
|
|
131
|
+
</Tab>
|
|
132
|
+
|
|
133
|
+
<Tab value="Resource">
|
|
134
|
+
|
|
135
|
+
`store.resource` builds five Flows. Mount with
|
|
136
|
+
[`http.resource`](/docs/elements/flow/http#resources) — the factory registers no routes:
|
|
137
|
+
|
|
138
|
+
```typescript title="src/flows/notes/resource.ts"
|
|
139
|
+
import { store } from "okengine";
|
|
140
|
+
import { z } from "zod";
|
|
141
|
+
import { db, notes } from "@/schema";
|
|
142
|
+
|
|
143
|
+
export const notesResource = store.resource(db, notes, {
|
|
144
|
+
in: z.object({ title: z.string().min(1), body: z.string() }),
|
|
145
|
+
out: z.object({
|
|
146
|
+
id: z.string(),
|
|
147
|
+
title: z.string(),
|
|
148
|
+
body: z.string(),
|
|
149
|
+
}),
|
|
150
|
+
list: {
|
|
151
|
+
search: [notes.title],
|
|
152
|
+
filter: [notes.archivedAt],
|
|
153
|
+
cursor: [notes.createdAt, notes.id],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```typescript title="src/flows/notes/index.ts"
|
|
159
|
+
import { on, http } from "okengine";
|
|
160
|
+
import { member } from "@/core/gate";
|
|
161
|
+
import { notesResource } from "./resource";
|
|
162
|
+
|
|
163
|
+
export const notesApi = on(http.resource("/notes", notesResource.all()).gate(member));
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
</Tab>
|
|
167
|
+
|
|
168
|
+
<Tab value="RLS">
|
|
169
|
+
|
|
170
|
+
Pass extras as the **third argument array**. Helpers take SQL/JS column
|
|
171
|
+
**names** (strings) and stamp `oke.gate()` / `oke.user()` / `oke.has_scope()`
|
|
172
|
+
predicates:
|
|
173
|
+
|
|
174
|
+
```typescript title="src/db/schema.decl.ts"
|
|
175
|
+
import { store, field } from "okengine";
|
|
176
|
+
|
|
177
|
+
export const tasks = store.schema.table(
|
|
178
|
+
"tasks",
|
|
179
|
+
{
|
|
180
|
+
id: field.id().primaryKey(),
|
|
181
|
+
owner: field.text().notNull(),
|
|
182
|
+
title: field.text().notNull(),
|
|
183
|
+
},
|
|
184
|
+
[
|
|
185
|
+
store.schema.policy.gate("member", { for: "select" }),
|
|
186
|
+
store.schema.policy.owner("owner", { for: "all" }),
|
|
187
|
+
],
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
When `gate.auth.tenant` is on, every table needs
|
|
192
|
+
`store.schema.policy.tenant(…)` or `store.schema.unscoped()` — extract fails
|
|
193
|
+
otherwise.
|
|
194
|
+
|
|
195
|
+
</Tab>
|
|
196
|
+
|
|
197
|
+
</Tabs>
|
|
198
|
+
|
|
199
|
+
## Field Reference
|
|
200
|
+
|
|
201
|
+
Factories mirror Drizzle pg-core names. Chain modifiers after the factory:
|
|
202
|
+
|
|
203
|
+
| Factory | Infers | Notes |
|
|
204
|
+
| -------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
205
|
+
| `field.id()` | `string` | `text` + auto id on insert (`≡ field.text().okid()`) |
|
|
206
|
+
| `field.okid()` | `string` | Pins OK ID on a text column |
|
|
207
|
+
| `field.text()` / `varchar` / `char` | `string` | Optional `{ length?, enum? }` |
|
|
208
|
+
| `field.boolean()` | `boolean` | |
|
|
209
|
+
| `field.smallint()` / `integer()` | `number` | |
|
|
210
|
+
| `field.bigint({ mode? })` | `number` (default) | `mode`: `"number"` · `"bigint"` · `"string"` |
|
|
211
|
+
| `field.serial()` / `smallserial()` / `bigserial()` | `number` | NOT NULL by SQL physics |
|
|
212
|
+
| `field.numeric()` / `decimal()` | `string` (default) | Exact decimal; `{ precision?, scale?, mode? }` |
|
|
213
|
+
| `field.real()` / `doublePrecision()` | `number` | Float4 / float8 |
|
|
214
|
+
| `field.json()` / `jsonb()` | generic | Narrow with `field.json<MyShape>()` |
|
|
215
|
+
| `field.uuid()` | `string` | |
|
|
216
|
+
| `field.time()` / `timestamp()` / `date()` / `interval()` | see type | `timestamp` / `date` default to `Date`; `{ mode: "string" }` for ISO. On write, finite epoch-ms numbers (e.g. `fx.clock.now()`) coerce to `Date` for Postgres |
|
|
217
|
+
| `field.point()` / `line()` | tuple | `{ mode: "xy" }` / `"abc"` for objects |
|
|
218
|
+
| `field.bytea()` | `Buffer` | |
|
|
219
|
+
| `field.inet()` / `cidr()` / `macaddr()` / `macaddr8()` | `string` | |
|
|
220
|
+
|
|
221
|
+
| Chain | Meaning |
|
|
222
|
+
| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
|
223
|
+
| `.primaryKey()` · `.notNull()` · `.unique()` | Constraints |
|
|
224
|
+
| `.default(v)` · `.defaultFn(fn)` · `.now()` · `.okid()` | Defaults |
|
|
225
|
+
| `.pii()` · `.sensitive()` · `.retain(duration)` | Classification tags |
|
|
226
|
+
| `.searchable({ weight? })` · `.embed({ model?, dims? })` | Hybrid search — see [Search](/docs/elements/store/search) |
|
|
227
|
+
| `.as(sqlName)` · `.describe(text)` · `.type<T>()` | SQL name, docs, TS override |
|
|
228
|
+
| `.references(() => col, actions?)` | FK (`onDelete` / `onUpdate`: `cascade` · `restrict` · `no action` · `set null` · `set default`) |
|
|
229
|
+
|
|
230
|
+
## Declaring Stores
|
|
231
|
+
|
|
232
|
+
Bind tables once with `store.sql(name, options)`. The name becomes the resource
|
|
233
|
+
suffix (`sql:app`).
|
|
234
|
+
|
|
235
|
+
| Option | Type | Default | Meaning |
|
|
236
|
+
| ------------- | ----------------------- | ---------- | --------------------------------------------------- |
|
|
237
|
+
| `schema` | table map or module | _(omit)_ | Tables for push / migrate / `fx.store` |
|
|
238
|
+
| `classify` | `table → column → tags` | `{}` | Explicit tags; wins over schema-derived on conflict |
|
|
239
|
+
| `description` | `string` | store name | Console / docs label |
|
|
240
|
+
|
|
241
|
+
**Named keys** — pass the tables you care about:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
export const db = store.sql("app", { schema: { posts, notes } });
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Module star** — starters re-export everything from the decl file:
|
|
248
|
+
|
|
249
|
+
```typescript title="src/core.ts"
|
|
250
|
+
import { store } from "okengine";
|
|
251
|
+
import * as schema from "@/db/schema.decl";
|
|
252
|
+
|
|
253
|
+
export const db = store.sql("app", { schema });
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Consequence:** every `fx.store(db)` touch stamps `reads` / `writes` as
|
|
257
|
+
`sql:app` on the Flow — Manifest, Console, and least privilege follow from that.
|
|
258
|
+
|
|
259
|
+
## Session Handle
|
|
260
|
+
|
|
261
|
+
`fx.store(db)` returns a `SqlStoreHandle`. Every method goes through the same
|
|
262
|
+
masking / RLS / CDC boundary.
|
|
263
|
+
|
|
264
|
+
| Method | Purpose |
|
|
265
|
+
| ------------------------------------------------------------------- | -------------------------------------------------- |
|
|
266
|
+
| `select()` / `select(cols).from(t).where?.().orderBy?.().limit?.()` | Fluent select |
|
|
267
|
+
| `insert(t).values(row)` | Insert (`returning()` optional) |
|
|
268
|
+
| `update(t).set(row).where(cond)` | Update — `where` required |
|
|
269
|
+
| `findById(t, id)` | PK lookup |
|
|
270
|
+
| `delete(t, id)` / `delete(t).where(cond)` | Delete by PK or predicate |
|
|
271
|
+
| `exists(t, idOrWhere)` | Presence check |
|
|
272
|
+
| `upsert(t, matchOn, values, { onExisting? })` | Insert-or-skip; `"update"` opts into overwrite |
|
|
273
|
+
| `increment(t, id, column, by?)` | Atomic numeric bump (`by` default `1`) |
|
|
274
|
+
| `count(t, where?)` | `COUNT(*)` |
|
|
275
|
+
| `page(t, options)` | Offset or keyset page |
|
|
276
|
+
| `search(t, options)` | BM25 ± LSH — [Search](/docs/elements/store/search) |
|
|
277
|
+
| `raw(sql, params?)` | Parameterized SQL (`?` placeholders) |
|
|
278
|
+
|
|
279
|
+
Each verb binds with `await fx.store(db).<method>(…)` inside `do`:
|
|
280
|
+
|
|
281
|
+
<Tabs items={["Select", "Insert", "Update", "Delete", "Page", "Upsert"]}>
|
|
282
|
+
|
|
283
|
+
<Tab value="Select">
|
|
284
|
+
|
|
285
|
+
Fluent select, or `findById` for a PK:
|
|
286
|
+
|
|
287
|
+
```typescript title="src/flows/notes/[id]/get.ts"
|
|
288
|
+
import { on, flow, http } from "okengine";
|
|
289
|
+
import { z } from "zod";
|
|
290
|
+
import { eq } from "drizzle-orm";
|
|
291
|
+
import { db, notes } from "@/schema";
|
|
292
|
+
|
|
293
|
+
export const get = on(
|
|
294
|
+
http.get(),
|
|
295
|
+
flow({
|
|
296
|
+
in: z.object({ id: z.string() }),
|
|
297
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
298
|
+
errors: { NotFound: z.object({ id: z.string() }) },
|
|
299
|
+
do: async ({ id }, fx) => {
|
|
300
|
+
const [note] = await fx.store(db).select().from(notes).where(eq(notes.id, id));
|
|
301
|
+
if (!note) return fx.fail("NotFound", { id });
|
|
302
|
+
return note;
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`findById(notes, id)` is the same PK path without a fluent chain.
|
|
309
|
+
|
|
310
|
+
</Tab>
|
|
311
|
+
|
|
312
|
+
<Tab value="Insert">
|
|
313
|
+
|
|
314
|
+
```typescript title="src/flows/notes/create.ts"
|
|
315
|
+
import { on, flow, http } from "okengine";
|
|
316
|
+
import { z } from "zod";
|
|
317
|
+
import { db, notes } from "@/schema";
|
|
318
|
+
|
|
319
|
+
export const create = on(
|
|
320
|
+
http.post(),
|
|
321
|
+
flow({
|
|
322
|
+
in: z.object({ title: z.string().min(1) }),
|
|
323
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
324
|
+
do: async ({ title }, fx) => {
|
|
325
|
+
const id = fx.id();
|
|
326
|
+
const [row] = await fx.store(db).insert(notes).values({ id, title }).returning();
|
|
327
|
+
return fx.json.create(row);
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
</Tab>
|
|
334
|
+
|
|
335
|
+
<Tab value="Update">
|
|
336
|
+
|
|
337
|
+
`where` is required — bare updates throw
|
|
338
|
+
`update().set().where(): condition required`:
|
|
339
|
+
|
|
340
|
+
```typescript title="src/flows/notes/[id]/update.ts"
|
|
341
|
+
import { on, flow, http } from "okengine";
|
|
342
|
+
import { z } from "zod";
|
|
343
|
+
import { eq } from "drizzle-orm";
|
|
344
|
+
import { db, notes } from "@/schema";
|
|
345
|
+
|
|
346
|
+
export const update = on(
|
|
347
|
+
http.patch(),
|
|
348
|
+
flow({
|
|
349
|
+
in: z.object({
|
|
350
|
+
id: z.string(),
|
|
351
|
+
title: z.string().min(1).optional(),
|
|
352
|
+
}),
|
|
353
|
+
errors: { NotFound: z.object({ id: z.string() }) },
|
|
354
|
+
do: async ({ id, title }, fx) => {
|
|
355
|
+
if (title !== undefined) {
|
|
356
|
+
await fx.store(db).update(notes).set({ title }).where(eq(notes.id, id));
|
|
357
|
+
}
|
|
358
|
+
const row = await fx.store(db).findById(notes, id);
|
|
359
|
+
if (!row) return fx.fail("NotFound", { id });
|
|
360
|
+
return row;
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
</Tab>
|
|
367
|
+
|
|
368
|
+
<Tab value="Delete">
|
|
369
|
+
|
|
370
|
+
Two-arg form deletes by PK. Fluent `.where(cond)` also requires a condition
|
|
371
|
+
(`delete().where(): condition required`):
|
|
372
|
+
|
|
373
|
+
```typescript title="src/flows/notes/[id]/remove.ts"
|
|
374
|
+
import { on, flow, http } from "okengine";
|
|
375
|
+
import { z } from "zod";
|
|
376
|
+
import { db, notes } from "@/schema";
|
|
377
|
+
|
|
378
|
+
export const remove = on(
|
|
379
|
+
http.delete(),
|
|
380
|
+
flow({
|
|
381
|
+
in: z.object({ id: z.string() }),
|
|
382
|
+
do: async ({ id }, fx) => {
|
|
383
|
+
await fx.store(db).delete(notes, id);
|
|
384
|
+
return fx.json.empty();
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
</Tab>
|
|
391
|
+
|
|
392
|
+
<Tab value="Page">
|
|
393
|
+
|
|
394
|
+
`page` options: `where` · `orderBy` · `limit` · `offset` · `after` · `before`.
|
|
395
|
+
`after` / `before` cannot combine with each other or with `offset`.
|
|
396
|
+
|
|
397
|
+
```typescript title="src/flows/notes/list.ts"
|
|
398
|
+
import { on, flow, http } from "okengine";
|
|
399
|
+
import { db, notes } from "@/schema";
|
|
400
|
+
|
|
401
|
+
export const list = on(
|
|
402
|
+
http.get(),
|
|
403
|
+
flow({
|
|
404
|
+
do: async (_, fx) => {
|
|
405
|
+
return await fx.store(db).page(notes, {
|
|
406
|
+
where: { archivedAt: null },
|
|
407
|
+
orderBy: [notes.createdAt],
|
|
408
|
+
limit: 20,
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Consequence:** keyset pages (`after` / `before`) stay stable under inserts —
|
|
416
|
+
prefer them for live feeds. Offset is fine for admin tables.
|
|
417
|
+
|
|
418
|
+
</Tab>
|
|
419
|
+
|
|
420
|
+
<Tab value="Upsert">
|
|
421
|
+
|
|
422
|
+
Default is insert-once. Pass `{ onExisting: "update" }` to overwrite a match:
|
|
423
|
+
|
|
424
|
+
```typescript title="src/flows/notes/ensure.ts"
|
|
425
|
+
import { flow } from "okengine";
|
|
426
|
+
import { z } from "zod";
|
|
427
|
+
import { db, notes } from "@/schema";
|
|
428
|
+
|
|
429
|
+
export const ensureWelcome = flow("notes.ensureWelcome", {
|
|
430
|
+
in: z.object({ title: z.string() }),
|
|
431
|
+
do: async ({ title }, fx) => {
|
|
432
|
+
const result = await fx.store(db).upsert(
|
|
433
|
+
notes,
|
|
434
|
+
{ id: "welcome" },
|
|
435
|
+
{
|
|
436
|
+
id: "welcome",
|
|
437
|
+
title,
|
|
438
|
+
body: "Your Notes API is ready.",
|
|
439
|
+
archivedAt: null,
|
|
440
|
+
createdAt: new Date("2026-01-15T10:00:00.000Z"),
|
|
441
|
+
},
|
|
442
|
+
// { onExisting: "update" },
|
|
443
|
+
);
|
|
444
|
+
return result; // { status: "upserted" | "changed" | "already-existed" }
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Empty `matchOn` throws `upsert() requires at least one matchOn predicate`.
|
|
450
|
+
|
|
451
|
+
</Tab>
|
|
452
|
+
|
|
453
|
+
</Tabs>
|
|
454
|
+
|
|
455
|
+
Also on the handle: `exists` · `increment` · `count` · `raw` · `search`. See
|
|
456
|
+
[Search](/docs/elements/store/search) for hybrid ranking.
|
|
457
|
+
|
|
458
|
+
## Resources
|
|
459
|
+
|
|
460
|
+
<Callout title="Detailed section">
|
|
461
|
+
Mount details, live SSE, and verb tables live on [HTTP ·
|
|
462
|
+
Resources](/docs/elements/flow/http#resources). This section is the factory contract —
|
|
463
|
+
`store.resource(db, table, options)`.
|
|
464
|
+
</Callout>
|
|
465
|
+
|
|
466
|
+
`store.resource(db, table, options)` builds five Flows. Pass
|
|
467
|
+
`notesResource.all()` (or any bag with `list` · `create` · `get` · `update` ·
|
|
468
|
+
`remove`) to `on(http.resource(path, ops))`.
|
|
469
|
+
|
|
470
|
+
<Tabs items={["Define", "Mount"]}>
|
|
471
|
+
|
|
472
|
+
<Tab value="Define">
|
|
473
|
+
|
|
474
|
+
The factory registers no routes:
|
|
475
|
+
|
|
476
|
+
```typescript title="src/flows/notes/resource.ts"
|
|
477
|
+
import { store } from "okengine";
|
|
478
|
+
import { z } from "zod";
|
|
479
|
+
import { db, notes } from "@/schema";
|
|
480
|
+
|
|
481
|
+
export const notesResource = store.resource(db, notes, {
|
|
482
|
+
in: z.object({ title: z.string().min(1), body: z.string() }),
|
|
483
|
+
out: z.object({
|
|
484
|
+
id: z.string(),
|
|
485
|
+
title: z.string(),
|
|
486
|
+
body: z.string(),
|
|
487
|
+
}),
|
|
488
|
+
list: {
|
|
489
|
+
search: [notes.title],
|
|
490
|
+
filter: [notes.archivedAt],
|
|
491
|
+
cursor: [notes.createdAt, notes.id],
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
</Tab>
|
|
497
|
+
|
|
498
|
+
<Tab value="Mount">
|
|
499
|
+
|
|
500
|
+
`.gate(member)` stamps every verb. The client sees `api.notes.list` / `.create` /
|
|
501
|
+
`.get` / `.update` / `.remove` after `oke({ name: "app" }).adopt({ notes })`.
|
|
502
|
+
|
|
503
|
+
```typescript title="src/flows/notes/index.ts"
|
|
504
|
+
import { on, http } from "okengine";
|
|
505
|
+
import { member } from "@/core/gate";
|
|
506
|
+
import { notesResource } from "./resource";
|
|
507
|
+
|
|
508
|
+
export const notes = on(http.resource("/notes", notesResource.all()).gate(member));
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
</Tab>
|
|
512
|
+
|
|
513
|
+
</Tabs>
|
|
514
|
+
|
|
515
|
+
The URL id segment is always `:id`. Update is **PATCH**, not PUT.
|
|
516
|
+
|
|
517
|
+
| Op | Method | Path | Typical status |
|
|
518
|
+
| -------- | -------- | ------------- | ---------------------------------- |
|
|
519
|
+
| `list` | `GET` | `/notes` | `200` + `{ data, error, meta }` |
|
|
520
|
+
| `create` | `POST` | `/notes` | `201 Created` (`fx.json.create`) |
|
|
521
|
+
| `get` | `GET` | `/notes/:id` | `200`, or `NotFound` |
|
|
522
|
+
| `update` | `PATCH` | `/notes/:id` | `200`, or `NotFound` |
|
|
523
|
+
| `remove` | `DELETE` | `/notes/:id` | `204 No Content` (`fx.json.empty`) |
|
|
524
|
+
| `live` | `GET` | `/notes/live` | SSE — only when live is on |
|
|
525
|
+
|
|
526
|
+
<Accordions>
|
|
527
|
+
|
|
528
|
+
<Accordion title="Resource Options">
|
|
529
|
+
Third argument to `store.resource(db, table, options)`:
|
|
530
|
+
|
|
531
|
+
| Option | Type | Default | Meaning |
|
|
532
|
+
| ---------- | --------- | -------------------------------- | ------------------------------------------------------------------------ |
|
|
533
|
+
| `in` | Schema | _(required)_ | Create body (`POST`) |
|
|
534
|
+
| `out` | Schema | _(required)_ | Item shape (get / list / update return) |
|
|
535
|
+
| `update` | Schema | `in` | Patch fields. Wire body is `{ id, …patch }` |
|
|
536
|
+
| `idSchema` | Schema | `update`/`in` + `{ id: string }` | Replaces the update Flow `in` when set (include the id key) |
|
|
537
|
+
| `errors` | error map | `{ NotFound }` | Typed failures on get / update / remove |
|
|
538
|
+
| `id` | column | table PK | Column bound to `:id` |
|
|
539
|
+
| `list` | object | see List Options | List query grammar (`GET /notes`) |
|
|
540
|
+
| `breaking` | `boolean` | `false` | Marks the five Flows `breaking: true` (handwritten → resource migration) |
|
|
541
|
+
| `live` | `boolean` | omitted | Live query surface — see Resource Live |
|
|
542
|
+
|
|
543
|
+
</Accordion>
|
|
544
|
+
|
|
545
|
+
<Accordion title="List Options">
|
|
546
|
+
Nested on `store.resource(…, { list: { … } })`. Search / filter / order / select
|
|
547
|
+
use a column scope: `"all"` · column array · `"none"`.
|
|
548
|
+
|
|
549
|
+
| Option | Type | Default | Meaning |
|
|
550
|
+
| ----------- | ------------------------ | ------------------------------------------------ | ----------------------------------- |
|
|
551
|
+
| `mode` | `"cursor"` \| `"offset"` | `"cursor"` when `cursor` is set, else `"offset"` | Pagination |
|
|
552
|
+
| `cursor` | columns | `[]` | Keyset columns |
|
|
553
|
+
| `direction` | `"asc"` \| `"desc"` | `"desc"` | Default sort when no `?order=` |
|
|
554
|
+
| `limit` | `number` | `20` | Default page size |
|
|
555
|
+
| `maxLimit` | `number` | `100` | Cap on `?limit=` |
|
|
556
|
+
| `count` | `"exact"` \| `"none"` | `"exact"` | Offset-only `COUNT(*)` |
|
|
557
|
+
| `search` | column scope | `"none"` | `?search=` / `?q=` substring `LIKE` |
|
|
558
|
+
| `filter` | column scope | `"none"` | `?col=eq.x` grammar |
|
|
559
|
+
| `order` | column scope | cursor columns, else `"all"` | `?order=` |
|
|
560
|
+
| `select` | column scope | `"all"` | `?select=` projection |
|
|
561
|
+
|
|
562
|
+
</Accordion>
|
|
563
|
+
|
|
564
|
+
<Accordion title="List URL grammar">
|
|
565
|
+
Shared by resource lists, `liveQuery`, and handwritten pages that call
|
|
566
|
+
`parseListQuery`:
|
|
567
|
+
|
|
568
|
+
| Param | Example | Meaning |
|
|
569
|
+
| ----------------------------- | ----------------------- | --------------------------------------------------------------------------------------------- |
|
|
570
|
+
| `limit` / `offset` / `cursor` | `?limit=20` | Pagination |
|
|
571
|
+
| `search` / `q` | `?search=refund` | Substring `LIKE` (not BM25) |
|
|
572
|
+
| `order` | `?order=createdAt.desc` | Sort |
|
|
573
|
+
| `select` | `?select=id,title` | Projection |
|
|
574
|
+
| column filter | `?status=eq.active` | `eq` · `neq` · `gt` · `gte` · `lt` · `lte` · `like` · `ilike` · `in` · `is` (+ `not.` prefix) |
|
|
575
|
+
| `or` / `and` | `?or=(…)` | Nested boolean groups |
|
|
576
|
+
|
|
577
|
+
**Consequence:** list `?search=` is substring filter. Hybrid BM25 ranking is
|
|
578
|
+
`fx.store(db).search(table, { query })` — see [Two Surfaces](/docs/elements/store/search#two-surfaces).
|
|
579
|
+
|
|
580
|
+
</Accordion>
|
|
581
|
+
|
|
582
|
+
<Accordion title="Resource Members">
|
|
583
|
+
|
|
584
|
+
| Member | Kind | Meaning |
|
|
585
|
+
| ----------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
|
|
586
|
+
| `all()` | method, no args | Bag for `http.resource(path, notesResource.all())` — five Flows, plus `live` when enabled |
|
|
587
|
+
| `list` · `create` · `get` · `update` · `remove` | Flow | One verb. Bind with `http.get` / `http.post` / `http.patch` / `http.delete` |
|
|
588
|
+
| `page(input)` | method | Compile list-query input for a handwritten `fx.store(db).page` |
|
|
589
|
+
| `live` | `{ signal, flow }?` | Live surface when `live: true` (or the project default drained on) |
|
|
590
|
+
|
|
591
|
+
</Accordion>
|
|
592
|
+
|
|
593
|
+
<Accordion title="Resource Live">
|
|
594
|
+
A sixth route appears only when the resource is live. It is **not** a signal
|
|
595
|
+
firehose — each subscriber gets classified row events (RLS + list filters).
|
|
596
|
+
|
|
597
|
+
| `live` on the resource | Result |
|
|
598
|
+
| -------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
599
|
+
| `{ live: true }` | Mount `GET <path>/live` now |
|
|
600
|
+
| omitted | Mount only if `oke({ store: { live: true } })` |
|
|
601
|
+
| `{ live: false }` | Never mount live for this resource |
|
|
602
|
+
| table `store.schema.live(false)` | Opts that table out of the project default. `{ live: true }` on the resource still wins. |
|
|
603
|
+
|
|
604
|
+
```typescript title="src/flows/notes/resource.ts"
|
|
605
|
+
const notesResource = store.resource(db, notes, {
|
|
606
|
+
in: z.object({ title: z.string().min(1) }),
|
|
607
|
+
out: z.object({ id: z.string(), title: z.string() }),
|
|
608
|
+
live: true,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
export const notes = on(http.resource("/notes", notesResource.all()).gate(member));
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**Consequence:** `GET /notes/live` rides the same `.gate(...)` chain as list/get.
|
|
615
|
+
|
|
616
|
+
Wire events (consumed with `useLiveQuery` on the [typed client](/docs/client/react)):
|
|
617
|
+
|
|
618
|
+
| `kind` | Meaning |
|
|
619
|
+
| --------- | ----------------------------------------------------------- |
|
|
620
|
+
| `upsert` | Row visible under stamp + query — merge by primary key |
|
|
621
|
+
| `revoked` | Row left visibility (`reason: "rls"` or `"query"`) — remove |
|
|
622
|
+
| `delete` | Row deleted — remove |
|
|
623
|
+
|
|
624
|
+
Live queries need an RLS-capable SQL driver (`postgres` / `pglite`) and a gated
|
|
625
|
+
identity on the request. Extract fails without a primary key:
|
|
626
|
+
|
|
627
|
+
```text
|
|
628
|
+
extract: live: true on table "notes" requires a primary key column (upsert/revoked/delete address rows by PK)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Missing `updatedAt` / `updated_at`, or no RLS policies, warn at extract — they
|
|
632
|
+
do not fail the build. Full mount physics: [HTTP · Resource Live](/docs/elements/flow/http#resources).
|
|
633
|
+
|
|
634
|
+
</Accordion>
|
|
635
|
+
|
|
636
|
+
<Accordion title="Subset & Override">
|
|
637
|
+
`http.resource` always mounts all five CRUD keys. Bind individual Flows for a subset; spread
|
|
638
|
+
`.all()` and override one key to replace a verb. Collision errors (**OKE1041**): [HTTP ·
|
|
639
|
+
Resources](/docs/elements/flow/http#resources).
|
|
640
|
+
</Accordion>
|
|
641
|
+
|
|
642
|
+
</Accordions>
|
|
643
|
+
|
|
644
|
+
## Schema Extras & RLS
|
|
645
|
+
|
|
646
|
+
<Callout title="Detailed section">
|
|
647
|
+
If you only need a gate + owner policy, jump to Progressive Patterns → RLS. Extras are the **third
|
|
648
|
+
argument array** on `store.schema.table(name, cols, extras)`.
|
|
649
|
+
</Callout>
|
|
650
|
+
|
|
651
|
+
Pass extras after the column map:
|
|
652
|
+
|
|
653
|
+
| Extra | Meaning |
|
|
654
|
+
| ---------------------------------- | ---------------------------------------------------- |
|
|
655
|
+
| `store.schema.rls()` | Enable RLS on the table (no policies yet) |
|
|
656
|
+
| `store.schema.policy(name, opts?)` | Named policy (`for`, `as`, `using`, `withCheck`) |
|
|
657
|
+
| `store.schema.policy.gate(name)` | Default `for: "select"` — `oke.gate() = '…'` |
|
|
658
|
+
| `store.schema.policy.owner(col)` | Default `for: "all"` — `col = oke.user()` |
|
|
659
|
+
| `store.schema.policy.scope(scope)` | Default `for: "insert"` — `oke.has_scope('…')` |
|
|
660
|
+
| `store.schema.policy.tenant(col)` | Tenant predicate when tenancy is on |
|
|
661
|
+
| `store.schema.unscoped()` | Opt out of tenant requirement |
|
|
662
|
+
| `store.schema.live(false)` | Opt this table out of project live default |
|
|
663
|
+
| `store.schema.relations(…)` | Relation metadata for Drizzle emit — not a query API |
|
|
664
|
+
|
|
665
|
+
<Accordions>
|
|
666
|
+
|
|
667
|
+
<Accordion title="Policy helpers">
|
|
668
|
+
Helpers take **string** column / gate / scope names and stamp predicates:
|
|
669
|
+
|
|
670
|
+
```typescript title="src/db/schema.decl.ts"
|
|
671
|
+
export const bookings = store.schema.table(
|
|
672
|
+
"bookings",
|
|
673
|
+
{
|
|
674
|
+
id: field.id().primaryKey(),
|
|
675
|
+
owner: field.text().notNull(),
|
|
676
|
+
tenantId: field.text().notNull(),
|
|
677
|
+
},
|
|
678
|
+
[
|
|
679
|
+
store.schema.policy.gate("member", { for: "select" }),
|
|
680
|
+
store.schema.policy.owner("owner", { for: "all" }),
|
|
681
|
+
store.schema.policy.scope("booking:create", { for: "insert" }),
|
|
682
|
+
store.schema.policy.tenant("tenantId"),
|
|
683
|
+
],
|
|
684
|
+
);
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
`for` accepts SQL commands (`select` · `insert` · `update` · `delete` · `all`).
|
|
688
|
+
Raw `store.schema.policy(name, { using, withCheck })` is available when helpers
|
|
689
|
+
are not enough.
|
|
690
|
+
|
|
691
|
+
</Accordion>
|
|
692
|
+
|
|
693
|
+
<Accordion title="Tenant requirement">
|
|
694
|
+
When `gate.auth.tenant` is on, every table needs a tenant policy or an
|
|
695
|
+
explicit opt-out. Extract fails otherwise:
|
|
696
|
+
|
|
697
|
+
```text
|
|
698
|
+
extract: table "{store}.{table}" needs store.schema.policy.tenant(...) or store.schema.unscoped() when gate.auth.tenant is on
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
export const shared = store.schema.table(
|
|
703
|
+
"shared_flags",
|
|
704
|
+
{ id: field.id().primaryKey(), key: field.text().notNull() },
|
|
705
|
+
[store.schema.unscoped()],
|
|
706
|
+
);
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
</Accordion>
|
|
710
|
+
|
|
711
|
+
<Accordion title="Foreign keys & relations">
|
|
712
|
+
Column FKs use `.references(() => other.col, { onDelete?, onUpdate? })`.
|
|
713
|
+
`store.schema.relations` records one/many links for Drizzle emit — it does
|
|
714
|
+
**not** add `fx.store(db).query.…`:
|
|
715
|
+
|
|
716
|
+
```typescript title="src/db/schema.decl.ts"
|
|
717
|
+
export const links = store.schema.table("links", {
|
|
718
|
+
id: field.id().primaryKey(),
|
|
719
|
+
code: field.text().notNull().unique(),
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
export const daily = store.schema.table("daily", {
|
|
723
|
+
id: field.id().primaryKey(),
|
|
724
|
+
code: field
|
|
725
|
+
.text()
|
|
726
|
+
.notNull()
|
|
727
|
+
.references(() => links.code, { onDelete: "cascade" }),
|
|
728
|
+
day: field.text().notNull(),
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
export const relations = store.schema.relations({ links, daily }, (r) => ({
|
|
732
|
+
links: {
|
|
733
|
+
daily: r.many.daily({ from: r.links.code, to: r.daily.code }),
|
|
734
|
+
},
|
|
735
|
+
daily: {
|
|
736
|
+
link: r.one.links({
|
|
737
|
+
from: r.daily.code,
|
|
738
|
+
to: r.links.code,
|
|
739
|
+
optional: false,
|
|
740
|
+
}),
|
|
741
|
+
},
|
|
742
|
+
}));
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Join in Flows with fluent `select` / `where`, or mount related tables as their
|
|
746
|
+
own resources.
|
|
747
|
+
|
|
748
|
+
</Accordion>
|
|
749
|
+
|
|
750
|
+
</Accordions>
|
|
751
|
+
|
|
752
|
+
## Seeding
|
|
753
|
+
|
|
754
|
+
<StoreSeeding />
|
|
755
|
+
|
|
756
|
+
Seed never runs at boot without confirmation — only via `oke db seed`, or the
|
|
757
|
+
first-boot confirm when a seed module exists and `.oke/state.json` has not
|
|
758
|
+
recorded that identity yet. Categories:
|
|
759
|
+
|
|
760
|
+
| Block | Runs when |
|
|
761
|
+
| -------------- | ----------------------- |
|
|
762
|
+
| `essential` | Every env |
|
|
763
|
+
| `dev` | `dev` ConfigEnv only |
|
|
764
|
+
| `prod` | `prod` only |
|
|
765
|
+
| _(none extra)_ | `test` — essential only |
|
|
766
|
+
|
|
767
|
+
```typescript title="src/db/seed/index.ts"
|
|
768
|
+
import { defineSeed, type Fx } from "okengine";
|
|
769
|
+
import { db } from "@/core";
|
|
770
|
+
import { notes } from "@/db/schema.decl";
|
|
771
|
+
|
|
772
|
+
async function welcomeNote(fx: Fx) {
|
|
773
|
+
await fx.store(db).upsert(
|
|
774
|
+
notes,
|
|
775
|
+
{ id: "welcome" },
|
|
776
|
+
{
|
|
777
|
+
id: "welcome",
|
|
778
|
+
title: "Welcome",
|
|
779
|
+
body: "Your Notes API is ready.",
|
|
780
|
+
archivedAt: null,
|
|
781
|
+
createdAt: new Date("2026-01-15T10:00:00.000Z"),
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export default defineSeed({
|
|
787
|
+
name: "notes",
|
|
788
|
+
description: "Welcome + sample notes",
|
|
789
|
+
essential: welcomeNote,
|
|
790
|
+
// dev: sampleNotes,
|
|
791
|
+
// prod: async (fx) => { /* prod-only rows */ },
|
|
792
|
+
});
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
`upsert` returns `{ status: "upserted" | "changed" | "already-existed" }`.
|
|
796
|
+
Default is insert-once; pass `{ onExisting: "update" }` to overwrite.
|
|
797
|
+
|
|
798
|
+
## CDC
|
|
799
|
+
|
|
800
|
+
<Callout title="Detailed section">
|
|
801
|
+
Full consumer patterns live on [Consumers · CDC](/docs/elements/flow/consumers#cdc). Here is the
|
|
802
|
+
SQL-side trigger: `db.table(handle).changed(column?)`.
|
|
803
|
+
</Callout>
|
|
804
|
+
|
|
805
|
+
`db.table(orders).changed(column?)` builds a CDC trigger for `on(…)`. Input is
|
|
806
|
+
always `{ before, after }`. `changed("status")` stamps a **column** name on the
|
|
807
|
+
Manifest — it is not an op filter.
|
|
808
|
+
|
|
809
|
+
```typescript title="src/flows/orders/on-status.ts"
|
|
810
|
+
import { on, flow } from "okengine";
|
|
811
|
+
import { db, orders } from "@/schema";
|
|
812
|
+
|
|
813
|
+
export const onStatus = on(
|
|
814
|
+
db.table(orders).changed("status"),
|
|
815
|
+
flow("orders.statusChanged", {
|
|
816
|
+
do: async ({ before, after }, fx) => {
|
|
817
|
+
if (before?.status === after?.status) return;
|
|
818
|
+
/* committed write */
|
|
819
|
+
},
|
|
820
|
+
}),
|
|
821
|
+
);
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
**Consequence:** writes must go through `fx.store`. A raw SQL client bypasses
|
|
825
|
+
the CDC sink, so no consumer runs.
|
|
826
|
+
|
|
827
|
+
## Drivers
|
|
828
|
+
|
|
829
|
+
| Driver | Runs as | Best for |
|
|
830
|
+
| ---------- | ------------------------- | ------------------------------ |
|
|
831
|
+
| `postgres` | Docker / managed Postgres | Dev + prod default; RLS + live |
|
|
832
|
+
| `pglite` | In-process WASM | Test default; RLS-capable |
|
|
833
|
+
| `memory` | Process map | Tiny ephemeral tests |
|
|
834
|
+
|
|
835
|
+
Standard starters inherit `DRIVER_DEFAULTS` (`postgres` / `pglite` /
|
|
836
|
+
`postgres`). Pin only when you diverge; vendor choice lives under `images`, not
|
|
837
|
+
driver ids:
|
|
838
|
+
|
|
839
|
+
```typescript title="oke.config.ts"
|
|
840
|
+
import { defineConfig } from "okengine/config";
|
|
841
|
+
|
|
842
|
+
export default defineConfig({
|
|
843
|
+
drivers: {
|
|
844
|
+
// omit store.sql to use defaults — pin only overrides
|
|
845
|
+
},
|
|
846
|
+
images: {
|
|
847
|
+
store: {
|
|
848
|
+
sql: "postgres:18-alpine",
|
|
849
|
+
},
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
## Troubleshooting
|
|
855
|
+
|
|
856
|
+
<Accordions>
|
|
857
|
+
|
|
858
|
+
<Accordion title="OKE1110 — domain table not found">
|
|
859
|
+
Cause: `domain table not found — migrations have not been applied.` Run `oke db push` locally or
|
|
860
|
+
`oke db migrate` in that environment.
|
|
861
|
+
</Accordion>
|
|
862
|
+
|
|
863
|
+
<Accordion title="update().set().where(): condition required">
|
|
864
|
+
Updates and deletes without a `where` are rejected (`delete().where(): condition required`). Pass
|
|
865
|
+
a Drizzle condition or equality map — or use `delete(table, id)` for PK deletes.
|
|
866
|
+
</Accordion>
|
|
867
|
+
|
|
868
|
+
<Accordion title="page(): after and before / offset and keyset cannot combine">
|
|
869
|
+
Keyset (`after` / `before`) and `offset` are mutually exclusive. Pick one pagination mode per
|
|
870
|
+
call.
|
|
871
|
+
</Accordion>
|
|
872
|
+
|
|
873
|
+
<Accordion title="upsert() requires at least one matchOn predicate">
|
|
874
|
+
Pass a non-empty equality map or Drizzle condition that identifies the row.
|
|
875
|
+
</Accordion>
|
|
876
|
+
|
|
877
|
+
<Accordion title="increment(): no row with …">
|
|
878
|
+
The PK was missing. Insert first, or guard with `exists` / `findById`.
|
|
879
|
+
</Accordion>
|
|
880
|
+
|
|
881
|
+
<Accordion title="extract: table needs tenant or unscoped">
|
|
882
|
+
Cause: `extract: table "{store}.{table}" needs store.schema.policy.tenant(...) or
|
|
883
|
+
store.schema.unscoped() when gate.auth.tenant is on`. Add a tenant policy or mark the table
|
|
884
|
+
unscoped.
|
|
885
|
+
</Accordion>
|
|
886
|
+
|
|
887
|
+
<Accordion title="live: true requires a primary key / RLS driver">
|
|
888
|
+
Extract: `live: true on table "…" requires a primary key column`. Runtime needs `postgres` /
|
|
889
|
+
`pglite` and a gated identity on the request.
|
|
890
|
+
</Accordion>
|
|
891
|
+
|
|
892
|
+
<Accordion title="unknown list param / search is not enabled">
|
|
893
|
+
Default `filter: "none"` / `search: "none"` rejects extra keys. Whitelist
|
|
894
|
+
columns on `store.resource(…, { list: { … } })` or your `liveQuery` options.
|
|
895
|
+
</Accordion>
|
|
896
|
+
|
|
897
|
+
<Accordion title="fx.store(db).query is undefined">
|
|
898
|
+
There is no Relational Query Builder on the session handle. Use fluent `select` / `insert` /
|
|
899
|
+
`update` / `page`, or `store.resource` for CRUD. `store.schema.relations` is emit metadata only.
|
|
900
|
+
</Accordion>
|
|
901
|
+
|
|
902
|
+
<Accordion title="OKE1041 — method + path bound twice">
|
|
903
|
+
A resource mount plus a handwritten `http.get("/notes")` (or two mounts on the same base path)
|
|
904
|
+
collide. Drop one binding — see [HTTP ·
|
|
905
|
+
Troubleshooting](/docs/elements/flow/http#troubleshooting).
|
|
906
|
+
</Accordion>
|
|
907
|
+
|
|
908
|
+
</Accordions>
|
|
909
|
+
|
|
910
|
+
## Learn more
|
|
911
|
+
|
|
912
|
+
- [Store](/docs/elements/store) — four facets; driver defaults
|
|
913
|
+
- [Search](/docs/elements/store/search) — `.searchable()` / `.embed()` / `fx.store(db).search`
|
|
914
|
+
- [HTTP · Resources](/docs/elements/flow/http#resources) — mount, live SSE, verb table
|
|
915
|
+
- [Consumers · CDC](/docs/elements/flow/consumers#cdc) — `db.table(…).changed()`
|
|
916
|
+
- [fx](/docs/reference/fx) — `fx.store` session
|
|
917
|
+
- [Errors](/docs/reference/errors) — OKE1110 · OKE1041
|
|
918
|
+
|
|
919
|
+
## Next
|
|
920
|
+
|
|
921
|
+
<Cards>
|
|
922
|
+
<Card
|
|
923
|
+
title="KV"
|
|
924
|
+
description="Namespaced get/set with duration TTL."
|
|
925
|
+
href="/docs/elements/store/kv"
|
|
926
|
+
/>
|
|
927
|
+
<Card
|
|
928
|
+
title="Search"
|
|
929
|
+
description="BM25 ± LSH on SQL columns."
|
|
930
|
+
href="/docs/elements/store/search"
|
|
931
|
+
/>
|
|
932
|
+
<Card
|
|
933
|
+
title="HTTP · Resources"
|
|
934
|
+
description="Mount store.resource as five CRUD verbs + live."
|
|
935
|
+
href="/docs/elements/flow/http#resources"
|
|
936
|
+
/>
|
|
937
|
+
</Cards>
|