okengine 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/manifest.v1.schema.json +21 -2
  2. package/package.json +1 -1
  3. package/site/content/docs/elements/clock.mdx +7 -0
  4. package/site/content/docs/elements/flow.mdx +20 -12
  5. package/site/content/docs/elements/gate.mdx +108 -3
  6. package/site/content/docs/elements/signal.mdx +46 -17
  7. package/site/content/docs/elements/store.mdx +36 -32
  8. package/site/content/docs/elements/vault.mdx +26 -0
  9. package/site/content/docs/get-started/project-structure.mdx +4 -4
  10. package/site/content/docs/reference/cli.md +1 -1
  11. package/site/content/docs/reference/client.mdx +75 -17
  12. package/site/content/docs/reference/configuration.mdx +7 -4
  13. package/site/content/docs/reference/errors.mdx +24 -17
  14. package/site/content/docs/reference/fx.mdx +39 -22
  15. package/site/content/docs/reference/security.md +1 -1
  16. package/src/auth/api-key-sql.test.ts +47 -0
  17. package/src/auth/api-key-sql.ts +261 -0
  18. package/src/auth/api-keys.ts +276 -14
  19. package/src/auth/auth.test.ts +56 -0
  20. package/src/auth/config.ts +24 -5
  21. package/src/auth/index.ts +62 -1
  22. package/src/auth/plugin.ts +6 -1
  23. package/src/auth/schema.ts +8 -0
  24. package/src/auth/sessions.ts +18 -0
  25. package/src/auth/tables.ts +32 -0
  26. package/src/auth/tenant-config.ts +74 -0
  27. package/src/auth/tenant-tables.ts +11 -0
  28. package/src/auth/tenants.test.ts +63 -0
  29. package/src/auth/tenants.ts +360 -0
  30. package/src/cli/attach-host-console.test.ts +37 -0
  31. package/src/cli/attach-host-console.ts +6 -0
  32. package/src/cli/build.ts +2 -2
  33. package/src/client/budget.test.ts +1 -1
  34. package/src/client/create.ts +75 -5
  35. package/src/client/index.ts +13 -0
  36. package/src/client/live.test.ts +422 -0
  37. package/src/client/live.ts +389 -0
  38. package/src/client/notes-contract.test.ts +41 -0
  39. package/src/client/types.ts +85 -6
  40. package/src/client-react/index.ts +85 -1
  41. package/src/client-react/use-live.test.ts +129 -0
  42. package/src/compiler/effects-infer.ts +37 -2
  43. package/src/compiler/extract.test.ts +143 -11
  44. package/src/compiler/extract.ts +210 -30
  45. package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +1 -2
  46. package/src/compiler/fixtures/skyport.expected.json +2 -3
  47. package/src/compiler/response.ts +41 -11
  48. package/src/console/server/access.test.ts +116 -8
  49. package/src/console/server/access.ts +70 -23
  50. package/src/console/server/app.ts +45 -11
  51. package/src/console/server/console.test.ts +6 -8
  52. package/src/console/server/dev-identities.test.ts +127 -0
  53. package/src/console/server/dev-identities.ts +229 -0
  54. package/src/console/server/flows-invoke.test.ts +21 -4
  55. package/src/console/server/flows.ts +113 -58
  56. package/src/console/server/gates.test.ts +24 -0
  57. package/src/console/server/gates.ts +10 -30
  58. package/src/console/server/index.ts +1 -0
  59. package/src/console/server/live.test.ts +26 -1
  60. package/src/console/server/live.ts +4 -2
  61. package/src/console/server/operator-db.ts +2 -0
  62. package/src/console/server/state.ts +29 -55
  63. package/src/console/server/store.test.ts +17 -0
  64. package/src/console/server/store.ts +43 -1
  65. package/src/console/ui-next/dist/assets/access-page-CXVWWMmD.js +4 -0
  66. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  67. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  68. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  69. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  70. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  71. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  72. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  73. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  74. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  75. package/src/console/ui-next/dist/assets/flows-page-XDpAJO8f.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-BBj2QJCu.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  80. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  81. package/src/console/ui-next/dist/assets/observability-page-BFaay44m.js +4 -0
  82. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  83. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  84. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-CS5-aETQ.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BtyhmWb4.js +55 -0
  90. package/src/console/ui-next/dist/assets/units-page-Ca2Z-E52.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  92. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  93. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  94. package/src/console/ui-next/dist/assets/vault-page-BmOeAFwg.js +2 -0
  95. package/src/console/ui-next/dist/index.html +11 -9
  96. package/src/console/ui-next/index.html +3 -3
  97. package/src/console/ui-next/src/client.ts +167 -0
  98. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  99. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  100. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  101. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  102. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  103. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  104. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  105. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  106. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  107. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  108. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  109. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  110. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  111. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  112. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  113. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  114. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  115. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  116. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  117. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  118. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  119. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  120. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  121. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  122. package/src/console/ui-next/src/features/flows/fixture.ts +0 -1
  123. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  124. package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +5 -1
  125. package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +15 -4
  126. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  127. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  128. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  129. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  130. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  131. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  132. package/src/console/ui-next/src/router.tsx +10 -1
  133. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  134. package/src/console/ui-next/ui-next-seed-manifest-surface.ts +2 -9
  135. package/src/console/ui-next/ui-next-seed-manifest.ts +0 -1
  136. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  137. package/src/console/xss.gate.test.ts +1 -0
  138. package/src/drivers/index.ts +2 -0
  139. package/src/drivers/journal-postgres.ts +12 -3
  140. package/src/drivers/pg-rls.ts +26 -2
  141. package/src/drivers/pg-vault-rls.ts +68 -0
  142. package/src/drivers/signal-engine.ts +69 -15
  143. package/src/drivers/signal-live-iter.ts +65 -0
  144. package/src/drivers/signal-nats.ts +2 -1
  145. package/src/drivers/signal-postgres.ts +95 -12
  146. package/src/drivers/signal-redis.ts +2 -1
  147. package/src/drivers/signal-retention.ts +64 -0
  148. package/src/drivers/signal-types.ts +35 -5
  149. package/src/elements/clock/declare.ts +64 -2
  150. package/src/elements/clock/reconcile.ts +98 -25
  151. package/src/elements/clock/runtime.ts +13 -2
  152. package/src/elements/clock.test.ts +28 -0
  153. package/src/elements/clock.ts +9 -1
  154. package/src/elements/gate/declare.ts +2 -0
  155. package/src/elements/gate/permissions.ts +12 -0
  156. package/src/elements/gate.ts +6 -1
  157. package/src/elements/signal/declare.ts +44 -10
  158. package/src/elements/signal/delivery-modes.test.ts +90 -4
  159. package/src/elements/signal/order-lifecycle.test.ts +20 -4
  160. package/src/elements/signal/runtime.ts +42 -0
  161. package/src/elements/signal.test.ts +21 -8
  162. package/src/elements/signal.ts +1 -1
  163. package/src/elements/store/cache.ts +1 -1
  164. package/src/elements/store/declare.ts +7 -0
  165. package/src/elements/store/rls-identity.test.ts +29 -0
  166. package/src/elements/store/rls-identity.ts +3 -0
  167. package/src/elements/store/runtime.ts +14 -0
  168. package/src/elements/store/schema-decl.ts +162 -44
  169. package/src/elements/store/schema-tenant.ts +41 -0
  170. package/src/elements/store/sql-rls-isolation.test.ts +39 -0
  171. package/src/elements/store/sql-select.types.test.ts +71 -0
  172. package/src/elements/store/sql-session.ts +54 -32
  173. package/src/elements/store.ts +5 -0
  174. package/src/elements/vault/builtin-adapter.ts +15 -2
  175. package/src/elements/vault/declare.ts +8 -0
  176. package/src/elements/vault/runtime.ts +7 -0
  177. package/src/elements/vault/sql-rls-isolation.test.ts +145 -0
  178. package/src/elements/vault/storage.ts +9 -0
  179. package/src/elements/vault/test-helpers.ts +2 -1
  180. package/src/i18n/catalogs/ar.ts +19 -0
  181. package/src/i18n/catalogs/en.ts +19 -0
  182. package/src/index.ts +2 -0
  183. package/src/kernel/adopt-routes.ts +56 -8
  184. package/src/kernel/api-key-host-persist.test.ts +143 -0
  185. package/src/kernel/app-tenant.ts +122 -0
  186. package/src/kernel/app.ts +196 -57
  187. package/src/kernel/auth-resolve.ts +13 -5
  188. package/src/kernel/boot-bind/clock.ts +9 -3
  189. package/src/kernel/boot.test.ts +4 -4
  190. package/src/kernel/boot.ts +2 -0
  191. package/src/kernel/budget.test.ts +1 -1
  192. package/src/kernel/clock-durable.ts +8 -0
  193. package/src/kernel/clock-per-tenant-name.ts +5 -0
  194. package/src/kernel/clock-reconcile.ts +8 -0
  195. package/src/kernel/errors-live-resume.ts +15 -0
  196. package/src/kernel/errors-tenant.ts +29 -0
  197. package/src/kernel/errors.registry.test.ts +31 -3
  198. package/src/kernel/errors.ts +43 -3
  199. package/src/kernel/flow.ts +26 -5
  200. package/src/kernel/fx-auth-keys.test.ts +88 -0
  201. package/src/kernel/fx-auth-keys.ts +203 -0
  202. package/src/kernel/fx-auth-tenants.test.ts +87 -0
  203. package/src/kernel/fx-auth-tenants.ts +286 -0
  204. package/src/kernel/fx-live-stream.ts +149 -0
  205. package/src/kernel/fx-live.test.ts +157 -0
  206. package/src/kernel/fx-runtime.ts +15 -0
  207. package/src/kernel/fx-tenant-store.ts +213 -0
  208. package/src/kernel/fx.test.ts +108 -1
  209. package/src/kernel/fx.ts +206 -28
  210. package/src/kernel/hooks.ts +2 -2
  211. package/src/kernel/http-resource.ts +9 -18
  212. package/src/kernel/index.ts +2 -0
  213. package/src/kernel/journal.ts +12 -0
  214. package/src/kernel/live-http.test.ts +78 -0
  215. package/src/kernel/live-http.ts +114 -0
  216. package/src/kernel/live-resume.test.ts +125 -0
  217. package/src/kernel/on.ts +51 -0
  218. package/src/kernel/pipeline-api-key.test.ts +192 -7
  219. package/src/kernel/pipeline-tenant.ts +49 -0
  220. package/src/kernel/pipeline.test.ts +1 -1
  221. package/src/kernel/pipeline.ts +41 -4
  222. package/src/kernel/resource-mount.test.ts +10 -27
  223. package/src/kernel/tenant-resolve.test.ts +101 -0
  224. package/src/kernel/tenant-resolve.ts +124 -0
  225. package/src/kernel/tenant-roles.test.ts +87 -0
  226. package/src/kernel/triggers.ts +59 -21
  227. package/src/manifest/diff.test.ts +11 -2
  228. package/src/manifest/diff.ts +53 -11
  229. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  230. package/src/manifest/fixtures/skyport.manifest.json +0 -1
  231. package/src/manifest/types.ts +58 -6
  232. package/src/release/build-lib.ts +14 -1
  233. package/src/release/limits.ts +2 -2
  234. package/src/release/measure.ts +55 -1
  235. package/src/client/live-gap.test.ts +0 -35
  236. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  237. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  238. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  239. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  240. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  241. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  242. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  243. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  244. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  245. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  246. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  247. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  248. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  249. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  250. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  251. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  252. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  253. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  254. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  255. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  256. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { ColumnClassification } from "../../manifest/types.ts";
9
+ import { lazyRequire } from "../../kernel/lazy-require.ts";
9
10
  import type { ColumnDef, TableHandle } from "./table.ts";
10
11
  import { id as idHelper, now as nowHelper } from "./table.ts";
11
12
 
@@ -38,8 +39,14 @@ export interface ColumnReference {
38
39
  * `getSQL` is a structural bridge so drizzle-orm operators (`eq`, `isNull`, …)
39
40
  * accept abstract columns in TypeScript. OKE’s SQL compiler never calls it —
40
41
  * it reads {@link SchemaColumnDecl.sqlName} / metadata directly.
42
+ *
43
+ * `TData` / `TNotNull` are type-level only (`$infer` is a phantom). They let
44
+ * {@link InferColumnJs} / table `$inferSelect` resolve real row shapes.
41
45
  */
42
- export interface SchemaColumnDecl extends ColumnDef {
46
+ export interface SchemaColumnDecl<
47
+ TData = unknown,
48
+ TNotNull extends boolean = boolean,
49
+ > extends ColumnDef {
43
50
  /** JS object key (e.g. `createdAt`). */
44
51
  readonly key: string;
45
52
  /** Database column name (e.g. `created_at`). */
@@ -47,7 +54,12 @@ export interface SchemaColumnDecl extends ColumnDef {
47
54
  /** Declared SQL type. */
48
55
  readonly sqlType: FieldSqlType;
49
56
  readonly primaryKey: boolean;
50
- readonly notNull: boolean;
57
+ readonly notNull: TNotNull;
58
+ /**
59
+ * Phantom JS value type — not present at runtime. Distinguishes `string`
60
+ * vs `number` so structural typing does not collapse column data types.
61
+ */
62
+ readonly $infer?: TData;
51
63
  readonly unique: boolean;
52
64
  /** Literal `.default(v)` when set. */
53
65
  readonly defaultValue?: string | number | boolean | null;
@@ -93,8 +105,14 @@ export interface SchemaRlsEnableDecl {
93
105
  readonly kind: "schema-rls";
94
106
  }
95
107
 
108
+ /** Opt out of tenant isolation for a schema table when tenancy is on. */
109
+ export interface SchemaTenantScopedDecl {
110
+ readonly kind: "schema-tenant-scoped";
111
+ readonly tenantScoped: false;
112
+ }
113
+
96
114
  /** Third-arg extra for {@link schemaTable}. */
97
- export type SchemaTableExtra = SchemaPolicyDecl | SchemaRlsEnableDecl;
115
+ export type SchemaTableExtra = SchemaPolicyDecl | SchemaRlsEnableDecl | SchemaTenantScopedDecl;
98
116
 
99
117
  /** Options for {@link schemaPolicy} / Gate helpers. */
100
118
  export interface SchemaPolicyOptions {
@@ -118,35 +136,45 @@ export interface SchemaTableDecl extends TableHandle {
118
136
  readonly rls?: boolean;
119
137
  /** Declared policies (emit + Manifest). */
120
138
  readonly policies?: readonly SchemaPolicyDecl[];
139
+ /** When `false`, skip fail-loud tenant-policy requirement. */
140
+ readonly tenantScoped?: boolean;
121
141
  }
122
142
 
123
- /** Fluent field builder before key finalization. */
124
- export interface FieldBuilder {
125
- primaryKey(): FieldBuilder;
126
- notNull(): FieldBuilder;
127
- unique(): FieldBuilder;
128
- default(value: string | number | boolean | null): FieldBuilder;
129
- defaultFn(fn: () => unknown): FieldBuilder;
130
- pii(): FieldBuilder;
131
- sensitive(): FieldBuilder;
132
- retain(duration: string): FieldBuilder;
143
+ /**
144
+ * Fluent field builder before key finalization.
145
+ *
146
+ * `TData` is the JS value type (`string` / `number`). `TNotNull` is `true`
147
+ * after `.notNull()` or `.primaryKey()` — matching runtime finalize.
148
+ */
149
+ export interface FieldBuilder<TData = unknown, TNotNull extends boolean = false> {
150
+ primaryKey(): FieldBuilder<TData, true>;
151
+ notNull(): FieldBuilder<TData, true>;
152
+ unique(): FieldBuilder<TData, TNotNull>;
153
+ default(value: string | number | boolean | null): FieldBuilder<TData, TNotNull>;
154
+ defaultFn(fn: () => unknown): FieldBuilder<TData, TNotNull>;
155
+ pii(): FieldBuilder<TData, TNotNull>;
156
+ sensitive(): FieldBuilder<TData, TNotNull>;
157
+ retain(duration: string): FieldBuilder<TData, TNotNull>;
133
158
  /** Override snake_case SQL name. */
134
- as(sqlName: string): FieldBuilder;
159
+ as(sqlName: string): FieldBuilder<TData, TNotNull>;
135
160
  /** Optional human description for Console / docs (falls back to the JS key). */
136
- describe(description: string): FieldBuilder;
161
+ describe(description: string): FieldBuilder<TData, TNotNull>;
137
162
  /**
138
163
  * Declare a foreign key to another column (dialect-agnostic).
139
164
  *
140
165
  * @param ref - Lazy target column (`() => links.code`)
141
166
  * @param actions - Optional ON DELETE / ON UPDATE
142
167
  */
143
- references(ref: () => SchemaColumnDecl, actions?: ReferenceActions): FieldBuilder;
168
+ references(
169
+ ref: () => SchemaColumnDecl,
170
+ actions?: ReferenceActions,
171
+ ): FieldBuilder<TData, TNotNull>;
144
172
  /**
145
173
  * Bind the JS key and produce a {@link SchemaColumnDecl}.
146
174
  *
147
175
  * @param key - Object key in the table column map
148
176
  */
149
- finalize(key: string): SchemaColumnDecl;
177
+ finalize(key: string): SchemaColumnDecl<TData, TNotNull>;
150
178
  }
151
179
 
152
180
  interface FieldState {
@@ -186,31 +214,39 @@ function mergeClassification(
186
214
  return { ...(current ?? {}), ...patch };
187
215
  }
188
216
 
189
- function createBuilder(state: FieldState): FieldBuilder {
190
- const next = (patch: Partial<FieldState>): FieldBuilder => createBuilder({ ...state, ...patch });
217
+ function createBuilder<TData, TNotNull extends boolean>(
218
+ state: FieldState,
219
+ ): FieldBuilder<TData, TNotNull> {
220
+ const next = <N extends boolean>(patch: Partial<FieldState>): FieldBuilder<TData, N> =>
221
+ createBuilder<TData, N>({ ...state, ...patch });
191
222
 
192
223
  return {
193
- primaryKey: () => next({ primaryKey: true, notNull: true }),
194
- notNull: () => next({ notNull: true }),
195
- unique: () => next({ unique: true }),
196
- default: (value) => next({ defaultValue: value }),
224
+ primaryKey: () => next<true>({ primaryKey: true, notNull: true }),
225
+ notNull: () => next<true>({ notNull: true }),
226
+ unique: () => next<TNotNull>({ unique: true }),
227
+ default: (value) => next<TNotNull>({ defaultValue: value }),
197
228
  defaultFn: (fn) =>
198
- next({
229
+ next<TNotNull>({
199
230
  defaultFn: fn,
200
231
  defaultFnKind: defaultFnKindOf(fn),
201
232
  }),
202
- pii: () => next({ classification: mergeClassification(state.classification, { pii: true }) }),
233
+ pii: () =>
234
+ next<TNotNull>({ classification: mergeClassification(state.classification, { pii: true }) }),
203
235
  sensitive: () =>
204
- next({ classification: mergeClassification(state.classification, { sensitive: true }) }),
236
+ next<TNotNull>({
237
+ classification: mergeClassification(state.classification, { sensitive: true }),
238
+ }),
205
239
  retain: (duration) =>
206
- next({ classification: mergeClassification(state.classification, { retain: duration }) }),
207
- as: (sqlName) => next({ sqlName }),
208
- describe: (description) => next({ description }),
240
+ next<TNotNull>({
241
+ classification: mergeClassification(state.classification, { retain: duration }),
242
+ }),
243
+ as: (sqlName) => next<TNotNull>({ sqlName }),
244
+ describe: (description) => next<TNotNull>({ description }),
209
245
  references: (ref, actions) =>
210
- next({
246
+ next<TNotNull>({
211
247
  references: actions ? { ref, actions } : { ref },
212
248
  }),
213
- finalize(key: string): SchemaColumnDecl {
249
+ finalize(key: string): SchemaColumnDecl<TData, TNotNull> {
214
250
  const sqlName = state.sqlName ?? camelToSnake(key);
215
251
  return {
216
252
  key,
@@ -218,7 +254,7 @@ function createBuilder(state: FieldState): FieldBuilder {
218
254
  sqlName,
219
255
  sqlType: state.sqlType,
220
256
  primaryKey: state.primaryKey,
221
- notNull: state.notNull || state.primaryKey,
257
+ notNull: (state.notNull || state.primaryKey) as TNotNull,
222
258
  unique: state.unique,
223
259
  ...(state.defaultValue !== undefined ? { defaultValue: state.defaultValue } : {}),
224
260
  ...(state.defaultFn ? { defaultFn: state.defaultFn } : {}),
@@ -240,15 +276,15 @@ function createBuilder(state: FieldState): FieldBuilder {
240
276
  * Field builders — `field.text()` / `field.integer()`.
241
277
  */
242
278
  export const field = {
243
- text: (): FieldBuilder =>
244
- createBuilder({
279
+ text: (): FieldBuilder<string, false> =>
280
+ createBuilder<string, false>({
245
281
  sqlType: "text",
246
282
  primaryKey: false,
247
283
  notNull: false,
248
284
  unique: false,
249
285
  }),
250
- integer: (): FieldBuilder =>
251
- createBuilder({
286
+ integer: (): FieldBuilder<number, false> =>
287
+ createBuilder<number, false>({
252
288
  sqlType: "integer",
253
289
  primaryKey: false,
254
290
  notNull: false,
@@ -256,8 +292,19 @@ export const field = {
256
292
  }),
257
293
  } as const;
258
294
 
259
- /** Column map input for {@link schemaTable}. */
260
- export type SchemaColumnInput = FieldBuilder | SchemaColumnDecl;
295
+ /**
296
+ * Column map input for {@link schemaTable}.
297
+ *
298
+ * Explicit `string` / `number` × nullability variants so `field.text()`
299
+ * (`FieldBuilder<string, false>`) remains assignable — methodful builders
300
+ * are invariant in their type parameters.
301
+ */
302
+ export type SchemaColumnInput =
303
+ | FieldBuilder<string, false>
304
+ | FieldBuilder<string, true>
305
+ | FieldBuilder<number, false>
306
+ | FieldBuilder<number, true>
307
+ | SchemaColumnDecl;
261
308
 
262
309
  /**
263
310
  * Whether a value is a finalized {@link SchemaColumnDecl}.
@@ -301,7 +348,7 @@ export function finalizeColumnMap(
301
348
  ): Record<string, SchemaColumnDecl> {
302
349
  const out: Record<string, SchemaColumnDecl> = {};
303
350
  for (const [key, value] of Object.entries(columns)) {
304
- out[key] = isFieldBuilder(value) ? value.finalize(key) : value;
351
+ out[key] = isFieldBuilder(value) ? value.finalize(key) : (value as SchemaColumnDecl);
305
352
  }
306
353
  return out;
307
354
  }
@@ -333,14 +380,34 @@ export function schemaTableSqlName(table: SchemaTableDecl): string | undefined {
333
380
  return undefined;
334
381
  }
335
382
 
383
+ /**
384
+ * JS value type for a finalized column (`string` / `number`, plus `| null`
385
+ * when the column is nullable).
386
+ */
387
+ export type InferColumnJs<C> =
388
+ C extends SchemaColumnDecl<infer D, infer N> ? (N extends true ? D : D | null) : unknown;
389
+
390
+ /**
391
+ * Finalize a {@link schemaTable} column input into a typed decl.
392
+ */
393
+ export type FinalizeColumn<T> =
394
+ T extends FieldBuilder<infer D, infer N>
395
+ ? SchemaColumnDecl<D, N>
396
+ : T extends SchemaColumnDecl<infer D, infer N>
397
+ ? SchemaColumnDecl<D, N>
398
+ : SchemaColumnDecl;
399
+
336
400
  /**
337
401
  * Table decl with columns as own properties (`links.code`) for FK / relations.
402
+ *
403
+ * `$inferSelect` is type-level only (Drizzle's own alias) — not set at runtime.
338
404
  */
339
405
  export type SchemaTableWithColumns<
340
406
  C extends Record<string, SchemaColumnDecl> = Record<string, SchemaColumnDecl>,
341
407
  > = Omit<SchemaTableDecl, "columns"> & {
342
408
  readonly columns: Readonly<C>;
343
- } & Omit<C, "kind" | "tableName">;
409
+ readonly $inferSelect: { [K in keyof C]: InferColumnJs<C[K]> };
410
+ } & Omit<C, "kind" | "tableName" | "$inferSelect">;
344
411
 
345
412
  /**
346
413
  * Declare an abstract schema table (ORM-agnostic).
@@ -356,7 +423,7 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
356
423
  name: string,
357
424
  columns: C,
358
425
  extras: readonly SchemaTableExtra[] = [],
359
- ): SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }> {
426
+ ): SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }> {
360
427
  const finalized = finalizeColumnMap(columns);
361
428
  const stamped: Record<string, SchemaColumnDecl> = {};
362
429
  for (const [key, col] of Object.entries(finalized)) {
@@ -366,18 +433,22 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
366
433
  (extra): extra is SchemaPolicyDecl => extra.kind === "schema-policy",
367
434
  );
368
435
  const rls = extras.some((extra) => extra.kind === "schema-rls") || policies.length > 0;
436
+ const unscoped = extras.some(
437
+ (extra) => extra.kind === "schema-tenant-scoped" && extra.tenantScoped === false,
438
+ );
369
439
  const table = {
370
440
  name,
371
441
  columns: stamped,
372
442
  ...stamped,
373
443
  ...(rls ? { rls: true } : {}),
374
444
  ...(policies.length > 0 ? { policies } : {}),
445
+ ...(unscoped ? { tenantScoped: false } : {}),
375
446
  };
376
447
  // Survive columns named `name` or `kind` (they would otherwise shadow
377
448
  // the table discriminant / SQL name).
378
449
  Object.defineProperty(table, "kind", { value: "schema-table", enumerable: false });
379
450
  Object.defineProperty(table, "tableName", { value: name, enumerable: false });
380
- return table as SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }>;
451
+ return table as SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }>;
381
452
  }
382
453
 
383
454
  /**
@@ -409,12 +480,25 @@ function sqlStringLiteral(value: string): string {
409
480
  return `'${value.replaceAll("'", "''")}'`;
410
481
  }
411
482
 
412
- function helperPolicyName(prefix: string, key: string, command: SchemaPolicyFor): string {
483
+ /**
484
+ * Policy name `prefix_key_command`.
485
+ *
486
+ * @param prefix - Helper kind
487
+ * @param key - Gate / column / scope
488
+ * @param command - SQL command
489
+ */
490
+ export function helperPolicyName(prefix: string, key: string, command: SchemaPolicyFor): string {
413
491
  const slug = key.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_|_$/g, "");
414
492
  return `${prefix}_${slug}_${command}`;
415
493
  }
416
494
 
417
- function policyPredicates(
495
+ /**
496
+ * USING / WITH CHECK split for a helper expression.
497
+ *
498
+ * @param command - SQL command
499
+ * @param expr - Predicate
500
+ */
501
+ export function policyPredicates(
418
502
  command: SchemaPolicyFor,
419
503
  expr: string,
420
504
  ): { readonly using?: string; readonly withCheck?: string } {
@@ -482,13 +566,46 @@ export type SchemaPolicyApi = typeof schemaPolicy & {
482
566
  readonly gate: typeof schemaPolicyGate;
483
567
  readonly owner: typeof schemaPolicyOwner;
484
568
  readonly scope: typeof schemaPolicyScope;
569
+ readonly tenant: (
570
+ column: string,
571
+ options?: Pick<SchemaPolicyOptions, "for" | "as" | "to">,
572
+ ) => SchemaPolicyDecl;
485
573
  };
486
574
 
575
+ function loadSchemaTenant(): {
576
+ tenant: SchemaPolicyApi["tenant"];
577
+ unscoped: () => SchemaTenantScopedDecl;
578
+ } {
579
+ return lazyRequire(import.meta.dir, ["schema", "tenant"].join("-"));
580
+ }
581
+
582
+ /**
583
+ * Tenant-column policy — `tenant_id = oke.tenant()`.
584
+ *
585
+ * @param column - SQL / JS column name
586
+ * @param options - Command (default `all`)
587
+ */
588
+ export function schemaPolicyTenant(
589
+ column: string,
590
+ options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
591
+ ): SchemaPolicyDecl {
592
+ return loadSchemaTenant().tenant(column, options);
593
+ }
594
+
595
+ /**
596
+ * Mark a table as globally shared (`tenantScoped: false`).
597
+ * Required when `gate.auth.tenant` is on and the table has no tenant policy.
598
+ */
599
+ export function schemaUnscoped(): SchemaTenantScopedDecl {
600
+ return loadSchemaTenant().unscoped();
601
+ }
602
+
487
603
  /** `store.schema.policy` — raw + Gate helpers. */
488
604
  export const schemaPolicyApi: SchemaPolicyApi = Object.assign(schemaPolicy, {
489
605
  gate: schemaPolicyGate,
490
606
  owner: schemaPolicyOwner,
491
607
  scope: schemaPolicyScope,
608
+ tenant: schemaPolicyTenant,
492
609
  });
493
610
 
494
611
  // ─── Relations (mirrors drizzle-orm `defineRelations`) ───────────────────────
@@ -688,6 +805,7 @@ export const schema = {
688
805
  relations: schemaRelations,
689
806
  rls: schemaRls,
690
807
  policy: schemaPolicyApi,
808
+ unscoped: schemaUnscoped,
691
809
  } as const;
692
810
 
693
811
  /**
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Tenant schema helpers — lazy chunk off Store-only `oke()` graphs.
3
+ *
4
+ * `store.schema.policy.tenant` / `store.schema.unscoped` stay on the public
5
+ * API via getters; this module is loaded on first access.
6
+ */
7
+
8
+ import {
9
+ schemaPolicy,
10
+ helperPolicyName,
11
+ policyPredicates,
12
+ type SchemaPolicyDecl,
13
+ type SchemaPolicyOptions,
14
+ type SchemaTenantScopedDecl,
15
+ } from "./schema-decl.ts";
16
+
17
+ /**
18
+ * Tenant-column policy — `tenant_id = oke.tenant()`.
19
+ *
20
+ * @param column - SQL / JS column name
21
+ * @param options - Command (default `all`)
22
+ */
23
+ export function tenant(
24
+ column: string,
25
+ options: Pick<SchemaPolicyOptions, "for" | "as" | "to"> = {},
26
+ ): SchemaPolicyDecl {
27
+ const command = options.for ?? "all";
28
+ return schemaPolicy(helperPolicyName("tenant", column, command), {
29
+ ...options,
30
+ for: command,
31
+ ...policyPredicates(command, `${column} = oke.tenant()`),
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Mark a table as globally shared (`tenantScoped: false`).
37
+ * Required when `gate.auth.tenant` is on and the table has no tenant policy.
38
+ */
39
+ export function unscoped(): SchemaTenantScopedDecl {
40
+ return { kind: "schema-tenant-scoped", tenantScoped: false };
41
+ }
@@ -100,6 +100,45 @@ describe("sql-session RLS isolation (pglite)", () => {
100
100
  { id: "b", owner: "bob" },
101
101
  ]);
102
102
  });
103
+
104
+ test("tenant_id = oke.tenant() isolates concurrent tenants", async () => {
105
+ await conn.exec(`CREATE TABLE bookings (
106
+ id text PRIMARY KEY,
107
+ tenant_id text NOT NULL,
108
+ body text NOT NULL
109
+ )`);
110
+ await conn.exec(`INSERT INTO bookings VALUES ('1', 'acme', 'a'), ('2', 'globex', 'b')`);
111
+ await conn.exec(`ALTER TABLE bookings ENABLE ROW LEVEL SECURITY`);
112
+ await conn.exec(`GRANT SELECT, INSERT, UPDATE, DELETE ON bookings TO oke_app`);
113
+ await conn.exec(`CREATE POLICY tenant_all ON bookings
114
+ AS PERMISSIVE FOR ALL TO public
115
+ USING (tenant_id = oke.tenant())
116
+ WITH CHECK (tenant_id = oke.tenant())`);
117
+
118
+ const asAcme = createSqlStoreHandle("sql:app", {
119
+ connection: conn,
120
+ classifications: new Map(),
121
+ routedRole: "primary",
122
+ domainDdl: "off",
123
+ rls: { gate: "member", userId: "u1", scopes: ["member"], tenantId: "acme" },
124
+ });
125
+ const asGlobex = createSqlStoreHandle("sql:app", {
126
+ connection: conn,
127
+ classifications: new Map(),
128
+ routedRole: "primary",
129
+ domainDdl: "off",
130
+ rls: { gate: "member", userId: "u1", scopes: ["member"], tenantId: "globex" },
131
+ });
132
+
133
+ const [acme, globex, acmeAgain] = await Promise.all([
134
+ asAcme.raw(`SELECT id, tenant_id FROM bookings ORDER BY id`),
135
+ asGlobex.raw(`SELECT id, tenant_id FROM bookings ORDER BY id`),
136
+ asAcme.raw(`SELECT id, tenant_id FROM bookings ORDER BY id`),
137
+ ]);
138
+ expect(acme).toEqual([{ id: "1", tenant_id: "acme" }]);
139
+ expect(globex).toEqual([{ id: "2", tenant_id: "globex" }]);
140
+ expect(acmeAgain).toEqual([{ id: "1", tenant_id: "acme" }]);
141
+ });
103
142
  });
104
143
 
105
144
  const LIVE_PG =
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Type-level proof: `select().from(store.schema.table())` resolves to the
3
+ * declared column JS shape — not `SqlRow`.
4
+ */
5
+ import { describe, expect, test } from "bun:test";
6
+
7
+ import type { SqlRow } from "../../drivers/types.ts";
8
+ import { field, schemaTable } from "./schema-decl.ts";
9
+ import type { InferSelectRow, SqlStoreHandle } from "./sql-session.ts";
10
+ import { defineTable } from "./table.ts";
11
+
12
+ /** Compile-time equality. */
13
+ type Eq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;
14
+ type Assert<T extends true> = T;
15
+
16
+ const items = schemaTable("items", {
17
+ id: field.text().primaryKey(),
18
+ title: field.text().notNull(),
19
+ note: field.text(),
20
+ count: field.integer().notNull(),
21
+ score: field.integer(),
22
+ });
23
+
24
+ type ExpectedRow = {
25
+ id: string;
26
+ title: string;
27
+ note: string | null;
28
+ count: number;
29
+ score: number | null;
30
+ };
31
+
32
+ async function selectAll(db: SqlStoreHandle) {
33
+ return db.select().from(items);
34
+ }
35
+
36
+ async function selectWhere(db: SqlStoreHandle) {
37
+ return db.select().from(items).where({ id: "x" });
38
+ }
39
+
40
+ async function selectOrder(db: SqlStoreHandle) {
41
+ return db.select().from(items).orderBy(items.title);
42
+ }
43
+
44
+ async function selectLimit(db: SqlStoreHandle) {
45
+ return db.select().from(items).limit(1);
46
+ }
47
+
48
+ async function selectProj(db: SqlStoreHandle) {
49
+ return db.select({ title: items.title }).from(items);
50
+ }
51
+
52
+ const bare = defineTable("bare", { id: { name: "id" } });
53
+
54
+ async function selectBare(db: SqlStoreHandle) {
55
+ return db.select().from(bare);
56
+ }
57
+
58
+ type _Infer = Assert<Eq<InferSelectRow<typeof items>, ExpectedRow>>;
59
+ type _All = Assert<Eq<Awaited<ReturnType<typeof selectAll>>, ExpectedRow[]>>;
60
+ type _Where = Assert<Eq<Awaited<ReturnType<typeof selectWhere>>, ExpectedRow[]>>;
61
+ type _Order = Assert<Eq<Awaited<ReturnType<typeof selectOrder>>, ExpectedRow[]>>;
62
+ type _Limit = Assert<Eq<Awaited<ReturnType<typeof selectLimit>>, ExpectedRow[]>>;
63
+ type _Proj = Assert<Eq<Awaited<ReturnType<typeof selectProj>>, SqlRow[]>>;
64
+ type _Bare = Assert<Eq<Awaited<ReturnType<typeof selectBare>>, SqlRow[]>>;
65
+
66
+ describe("select().from() row inference", () => {
67
+ test("type-level row shape matches declared columns", () => {
68
+ const _keep: _Infer & _All & _Where & _Order & _Limit & _Proj & _Bare = true;
69
+ expect(_keep).toBe(true);
70
+ });
71
+ });