okengine 0.15.2 → 0.16.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 (141) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/elements/flow.mdx +6 -0
  3. package/site/content/docs/elements/gate.mdx +55 -0
  4. package/site/content/docs/elements/store.mdx +13 -15
  5. package/site/content/docs/elements/vault.mdx +3 -0
  6. package/site/content/docs/reference/client.mdx +3 -0
  7. package/site/content/docs/reference/fx.mdx +29 -18
  8. package/site/content/docs/reference/security.md +1 -1
  9. package/src/auth/api-key-sql.test.ts +47 -0
  10. package/src/auth/api-key-sql.ts +254 -0
  11. package/src/auth/api-keys.ts +273 -14
  12. package/src/auth/auth.test.ts +56 -0
  13. package/src/auth/config.ts +5 -5
  14. package/src/auth/index.ts +25 -1
  15. package/src/auth/schema.ts +8 -0
  16. package/src/cli/attach-host-console.test.ts +37 -0
  17. package/src/cli/attach-host-console.ts +6 -0
  18. package/src/compiler/effects-infer.ts +17 -2
  19. package/src/console/server/access.test.ts +116 -8
  20. package/src/console/server/access.ts +70 -23
  21. package/src/console/server/app.ts +1 -0
  22. package/src/console/server/dev-identities.test.ts +127 -0
  23. package/src/console/server/dev-identities.ts +229 -0
  24. package/src/console/server/flows-invoke.test.ts +21 -4
  25. package/src/console/server/flows.ts +113 -58
  26. package/src/console/server/gates.test.ts +24 -0
  27. package/src/console/server/gates.ts +8 -21
  28. package/src/console/server/index.ts +1 -0
  29. package/src/console/server/live.test.ts +26 -1
  30. package/src/console/server/live.ts +4 -2
  31. package/src/console/server/operator-db.ts +2 -0
  32. package/src/console/server/state.ts +29 -55
  33. package/src/console/ui-next/dist/assets/access-page-CAGHrA9H.js +4 -0
  34. package/src/console/ui-next/dist/assets/{agent-disclosure-C0X1fbWF.js → agent-disclosure-BP0Y0Sux.js} +1 -1
  35. package/src/console/ui-next/dist/assets/cache-glyph-B5X-NM0-.js +1 -0
  36. package/src/console/ui-next/dist/assets/call-pii-button-ChAUiLo9.js +1 -0
  37. package/src/console/ui-next/dist/assets/collapsible-DGnOM2ph.js +1 -0
  38. package/src/console/ui-next/dist/assets/copy-inline-button-DBHhgHKP.js +1 -0
  39. package/src/console/ui-next/dist/assets/detail-header-DhHM1iaZ.js +1 -0
  40. package/src/console/ui-next/dist/assets/dropdown-menu-_NjTEo5_.js +1 -0
  41. package/src/console/ui-next/dist/assets/element-icons-BVXtRyd3.js +1 -0
  42. package/src/console/ui-next/dist/assets/explorer-empty-2uIhBu0_.js +1 -0
  43. package/src/console/ui-next/dist/assets/flows-page-B-OUtiAu.js +1 -0
  44. package/src/console/ui-next/dist/assets/{highlighted-json-CS_O8L-r.js → highlighted-json-Awq7gYdu.js} +1 -1
  45. package/src/console/ui-next/dist/assets/http-method-_UHM2ODJ.js +1 -0
  46. package/src/console/ui-next/dist/assets/index-CGoZkILK.js +66 -0
  47. package/src/console/ui-next/dist/assets/index-Ck88Jmv8.css +2 -0
  48. package/src/console/ui-next/dist/assets/link-BX6Vqztd.js +1 -0
  49. package/src/console/ui-next/dist/assets/observability-page-BDyXalNR.js +4 -0
  50. package/src/console/ui-next/dist/assets/replica-lag-DKRrbvdo.js +18 -0
  51. package/src/console/ui-next/dist/assets/request-meta-DV0ywz7t.js +1 -0
  52. package/src/console/ui-next/dist/assets/shortcut-JQIZlWfm.js +1 -0
  53. package/src/console/ui-next/dist/assets/shortcut-keys-DKxNTe_m.js +1 -0
  54. package/src/console/ui-next/dist/assets/skeleton-D-czQJT6.js +1 -0
  55. package/src/console/ui-next/dist/assets/store-page-Xh8Kn3rx.js +41 -0
  56. package/src/console/ui-next/dist/assets/trace-detail-sheet-B09O8rA6.js +2 -0
  57. package/src/console/ui-next/dist/assets/tree-expand-toggle-DkOXA12R.js +54 -0
  58. package/src/console/ui-next/dist/assets/units-page-Dpk40kOQ.js +1 -0
  59. package/src/console/ui-next/dist/assets/{use-vault-list-BLbZhzyF.js → use-vault-list-uk4WVboC.js} +1 -1
  60. package/src/console/ui-next/dist/assets/useMutation-B8EO02Ej.js +1 -0
  61. package/src/console/ui-next/dist/assets/useRender-BE2A9BWC.js +1 -0
  62. package/src/console/ui-next/dist/assets/vault-page-B1dB9Ft0.js +2 -0
  63. package/src/console/ui-next/dist/index.html +11 -9
  64. package/src/console/ui-next/index.html +3 -3
  65. package/src/console/ui-next/src/client.ts +167 -0
  66. package/src/console/ui-next/src/components/shell/app-sidebar.tsx +2 -0
  67. package/src/console/ui-next/src/components/shell/command-items.test.ts +1 -0
  68. package/src/console/ui-next/src/components/shell/command-items.ts +8 -0
  69. package/src/console/ui-next/src/components/shell/shell-layout.tsx +2 -1
  70. package/src/console/ui-next/src/components/ui/sheet-form.tsx +8 -3
  71. package/src/console/ui-next/src/features/access/access-page.tsx +347 -0
  72. package/src/console/ui-next/src/features/access/data/use-access.ts +23 -0
  73. package/src/console/ui-next/src/features/access/detail/access-detail.tsx +552 -0
  74. package/src/console/ui-next/src/features/access/explorer/access-list.tsx +125 -0
  75. package/src/console/ui-next/src/features/access/lib/format-when.test.ts +139 -0
  76. package/src/console/ui-next/src/features/access/lib/format-when.ts +296 -0
  77. package/src/console/ui-next/src/features/access/lib/scope-groups.test.ts +34 -0
  78. package/src/console/ui-next/src/features/access/lib/scope-groups.ts +40 -0
  79. package/src/console/ui-next/src/features/access/sheets/access-allow-fields.tsx +236 -0
  80. package/src/console/ui-next/src/features/access/sheets/access-create-sheet.tsx +316 -0
  81. package/src/console/ui-next/src/features/access/sheets/access-edit-sheet.tsx +240 -0
  82. package/src/console/ui-next/src/features/access/sheets/access-expiry-fields.tsx +66 -0
  83. package/src/console/ui-next/src/features/access/sheets/access-refresh-sheet.tsx +104 -0
  84. package/src/console/ui-next/src/features/access/sheets/access-scope-field.tsx +115 -0
  85. package/src/console/ui-next/src/features/access/sheets/access-secret-sheet.tsx +89 -0
  86. package/src/console/ui-next/src/features/access/sheets/access-sheet-search.tsx +49 -0
  87. package/src/console/ui-next/src/features/access/state/access-selection.ts +84 -0
  88. package/src/console/ui-next/src/features/auth/auth-redirect.test.ts +5 -0
  89. package/src/console/ui-next/src/features/auth/auth-redirect.ts +18 -2
  90. package/src/console/ui-next/src/features/not-found/not-found-page.tsx +1 -1
  91. package/src/console/ui-next/src/lib/document-meta.test.ts +1 -0
  92. package/src/console/ui-next/src/lib/document-meta.ts +2 -1
  93. package/src/console/ui-next/src/lib/last-module-search.test.ts +1 -0
  94. package/src/console/ui-next/src/lib/last-module-search.ts +2 -1
  95. package/src/console/ui-next/src/lib/shortcut.test.ts +2 -0
  96. package/src/console/ui-next/src/lib/shortcut.ts +3 -0
  97. package/src/console/ui-next/src/router.tsx +10 -1
  98. package/src/console/ui-next/ui-next-seed-app-schema.ts +88 -37
  99. package/src/console/ui-next/ui-next-seed-store.ts +43 -15
  100. package/src/console/xss.gate.test.ts +1 -0
  101. package/src/elements/gate/declare.ts +2 -0
  102. package/src/elements/store/cache.ts +1 -1
  103. package/src/elements/store/runtime.ts +14 -0
  104. package/src/elements/store/schema-decl.ts +99 -41
  105. package/src/elements/store/sql-select.types.test.ts +71 -0
  106. package/src/elements/store/sql-session.ts +54 -32
  107. package/src/elements/store.ts +3 -0
  108. package/src/index.ts +1 -0
  109. package/src/kernel/api-key-host-persist.test.ts +143 -0
  110. package/src/kernel/app.ts +14 -2
  111. package/src/kernel/auth-resolve.ts +10 -5
  112. package/src/kernel/boot.test.ts +4 -4
  113. package/src/kernel/fx-auth-keys.test.ts +88 -0
  114. package/src/kernel/fx-auth-keys.ts +198 -0
  115. package/src/kernel/fx.test.ts +17 -1
  116. package/src/kernel/fx.ts +36 -18
  117. package/src/kernel/pipeline-api-key.test.ts +192 -7
  118. package/src/kernel/pipeline.ts +4 -2
  119. package/src/manifest/types.ts +9 -3
  120. package/src/release/build-lib.ts +1 -0
  121. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +0 -1
  122. package/src/console/ui-next/dist/assets/call-pii-button-DEDCl_j3.js +0 -1
  123. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +0 -1
  124. package/src/console/ui-next/dist/assets/confirm-sheet-3ptDzXbz.js +0 -1
  125. package/src/console/ui-next/dist/assets/explorer-empty-BvYrygyO.js +0 -1
  126. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +0 -1
  127. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +0 -1
  128. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +0 -66
  129. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +0 -2
  130. package/src/console/ui-next/dist/assets/link-COeyggt-.js +0 -1
  131. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +0 -4
  132. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +0 -18
  133. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +0 -1
  134. package/src/console/ui-next/dist/assets/shortcut-Cgzuv4k1.js +0 -1
  135. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +0 -1
  136. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +0 -1
  137. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +0 -41
  138. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +0 -2
  139. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +0 -54
  140. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +0 -1
  141. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +0 -2
@@ -38,8 +38,14 @@ export interface ColumnReference {
38
38
  * `getSQL` is a structural bridge so drizzle-orm operators (`eq`, `isNull`, …)
39
39
  * accept abstract columns in TypeScript. OKE’s SQL compiler never calls it —
40
40
  * it reads {@link SchemaColumnDecl.sqlName} / metadata directly.
41
+ *
42
+ * `TData` / `TNotNull` are type-level only (`$infer` is a phantom). They let
43
+ * {@link InferColumnJs} / table `$inferSelect` resolve real row shapes.
41
44
  */
42
- export interface SchemaColumnDecl extends ColumnDef {
45
+ export interface SchemaColumnDecl<
46
+ TData = unknown,
47
+ TNotNull extends boolean = boolean,
48
+ > extends ColumnDef {
43
49
  /** JS object key (e.g. `createdAt`). */
44
50
  readonly key: string;
45
51
  /** Database column name (e.g. `created_at`). */
@@ -47,7 +53,12 @@ export interface SchemaColumnDecl extends ColumnDef {
47
53
  /** Declared SQL type. */
48
54
  readonly sqlType: FieldSqlType;
49
55
  readonly primaryKey: boolean;
50
- readonly notNull: boolean;
56
+ readonly notNull: TNotNull;
57
+ /**
58
+ * Phantom JS value type — not present at runtime. Distinguishes `string`
59
+ * vs `number` so structural typing does not collapse column data types.
60
+ */
61
+ readonly $infer?: TData;
51
62
  readonly unique: boolean;
52
63
  /** Literal `.default(v)` when set. */
53
64
  readonly defaultValue?: string | number | boolean | null;
@@ -120,33 +131,41 @@ export interface SchemaTableDecl extends TableHandle {
120
131
  readonly policies?: readonly SchemaPolicyDecl[];
121
132
  }
122
133
 
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;
134
+ /**
135
+ * Fluent field builder before key finalization.
136
+ *
137
+ * `TData` is the JS value type (`string` / `number`). `TNotNull` is `true`
138
+ * after `.notNull()` or `.primaryKey()` — matching runtime finalize.
139
+ */
140
+ export interface FieldBuilder<TData = unknown, TNotNull extends boolean = false> {
141
+ primaryKey(): FieldBuilder<TData, true>;
142
+ notNull(): FieldBuilder<TData, true>;
143
+ unique(): FieldBuilder<TData, TNotNull>;
144
+ default(value: string | number | boolean | null): FieldBuilder<TData, TNotNull>;
145
+ defaultFn(fn: () => unknown): FieldBuilder<TData, TNotNull>;
146
+ pii(): FieldBuilder<TData, TNotNull>;
147
+ sensitive(): FieldBuilder<TData, TNotNull>;
148
+ retain(duration: string): FieldBuilder<TData, TNotNull>;
133
149
  /** Override snake_case SQL name. */
134
- as(sqlName: string): FieldBuilder;
150
+ as(sqlName: string): FieldBuilder<TData, TNotNull>;
135
151
  /** Optional human description for Console / docs (falls back to the JS key). */
136
- describe(description: string): FieldBuilder;
152
+ describe(description: string): FieldBuilder<TData, TNotNull>;
137
153
  /**
138
154
  * Declare a foreign key to another column (dialect-agnostic).
139
155
  *
140
156
  * @param ref - Lazy target column (`() => links.code`)
141
157
  * @param actions - Optional ON DELETE / ON UPDATE
142
158
  */
143
- references(ref: () => SchemaColumnDecl, actions?: ReferenceActions): FieldBuilder;
159
+ references(
160
+ ref: () => SchemaColumnDecl,
161
+ actions?: ReferenceActions,
162
+ ): FieldBuilder<TData, TNotNull>;
144
163
  /**
145
164
  * Bind the JS key and produce a {@link SchemaColumnDecl}.
146
165
  *
147
166
  * @param key - Object key in the table column map
148
167
  */
149
- finalize(key: string): SchemaColumnDecl;
168
+ finalize(key: string): SchemaColumnDecl<TData, TNotNull>;
150
169
  }
151
170
 
152
171
  interface FieldState {
@@ -186,31 +205,39 @@ function mergeClassification(
186
205
  return { ...(current ?? {}), ...patch };
187
206
  }
188
207
 
189
- function createBuilder(state: FieldState): FieldBuilder {
190
- const next = (patch: Partial<FieldState>): FieldBuilder => createBuilder({ ...state, ...patch });
208
+ function createBuilder<TData, TNotNull extends boolean>(
209
+ state: FieldState,
210
+ ): FieldBuilder<TData, TNotNull> {
211
+ const next = <N extends boolean>(patch: Partial<FieldState>): FieldBuilder<TData, N> =>
212
+ createBuilder<TData, N>({ ...state, ...patch });
191
213
 
192
214
  return {
193
- primaryKey: () => next({ primaryKey: true, notNull: true }),
194
- notNull: () => next({ notNull: true }),
195
- unique: () => next({ unique: true }),
196
- default: (value) => next({ defaultValue: value }),
215
+ primaryKey: () => next<true>({ primaryKey: true, notNull: true }),
216
+ notNull: () => next<true>({ notNull: true }),
217
+ unique: () => next<TNotNull>({ unique: true }),
218
+ default: (value) => next<TNotNull>({ defaultValue: value }),
197
219
  defaultFn: (fn) =>
198
- next({
220
+ next<TNotNull>({
199
221
  defaultFn: fn,
200
222
  defaultFnKind: defaultFnKindOf(fn),
201
223
  }),
202
- pii: () => next({ classification: mergeClassification(state.classification, { pii: true }) }),
224
+ pii: () =>
225
+ next<TNotNull>({ classification: mergeClassification(state.classification, { pii: true }) }),
203
226
  sensitive: () =>
204
- next({ classification: mergeClassification(state.classification, { sensitive: true }) }),
227
+ next<TNotNull>({
228
+ classification: mergeClassification(state.classification, { sensitive: true }),
229
+ }),
205
230
  retain: (duration) =>
206
- next({ classification: mergeClassification(state.classification, { retain: duration }) }),
207
- as: (sqlName) => next({ sqlName }),
208
- describe: (description) => next({ description }),
231
+ next<TNotNull>({
232
+ classification: mergeClassification(state.classification, { retain: duration }),
233
+ }),
234
+ as: (sqlName) => next<TNotNull>({ sqlName }),
235
+ describe: (description) => next<TNotNull>({ description }),
209
236
  references: (ref, actions) =>
210
- next({
237
+ next<TNotNull>({
211
238
  references: actions ? { ref, actions } : { ref },
212
239
  }),
213
- finalize(key: string): SchemaColumnDecl {
240
+ finalize(key: string): SchemaColumnDecl<TData, TNotNull> {
214
241
  const sqlName = state.sqlName ?? camelToSnake(key);
215
242
  return {
216
243
  key,
@@ -218,7 +245,7 @@ function createBuilder(state: FieldState): FieldBuilder {
218
245
  sqlName,
219
246
  sqlType: state.sqlType,
220
247
  primaryKey: state.primaryKey,
221
- notNull: state.notNull || state.primaryKey,
248
+ notNull: (state.notNull || state.primaryKey) as TNotNull,
222
249
  unique: state.unique,
223
250
  ...(state.defaultValue !== undefined ? { defaultValue: state.defaultValue } : {}),
224
251
  ...(state.defaultFn ? { defaultFn: state.defaultFn } : {}),
@@ -240,15 +267,15 @@ function createBuilder(state: FieldState): FieldBuilder {
240
267
  * Field builders — `field.text()` / `field.integer()`.
241
268
  */
242
269
  export const field = {
243
- text: (): FieldBuilder =>
244
- createBuilder({
270
+ text: (): FieldBuilder<string, false> =>
271
+ createBuilder<string, false>({
245
272
  sqlType: "text",
246
273
  primaryKey: false,
247
274
  notNull: false,
248
275
  unique: false,
249
276
  }),
250
- integer: (): FieldBuilder =>
251
- createBuilder({
277
+ integer: (): FieldBuilder<number, false> =>
278
+ createBuilder<number, false>({
252
279
  sqlType: "integer",
253
280
  primaryKey: false,
254
281
  notNull: false,
@@ -256,8 +283,19 @@ export const field = {
256
283
  }),
257
284
  } as const;
258
285
 
259
- /** Column map input for {@link schemaTable}. */
260
- export type SchemaColumnInput = FieldBuilder | SchemaColumnDecl;
286
+ /**
287
+ * Column map input for {@link schemaTable}.
288
+ *
289
+ * Explicit `string` / `number` × nullability variants so `field.text()`
290
+ * (`FieldBuilder<string, false>`) remains assignable — methodful builders
291
+ * are invariant in their type parameters.
292
+ */
293
+ export type SchemaColumnInput =
294
+ | FieldBuilder<string, false>
295
+ | FieldBuilder<string, true>
296
+ | FieldBuilder<number, false>
297
+ | FieldBuilder<number, true>
298
+ | SchemaColumnDecl;
261
299
 
262
300
  /**
263
301
  * Whether a value is a finalized {@link SchemaColumnDecl}.
@@ -301,7 +339,7 @@ export function finalizeColumnMap(
301
339
  ): Record<string, SchemaColumnDecl> {
302
340
  const out: Record<string, SchemaColumnDecl> = {};
303
341
  for (const [key, value] of Object.entries(columns)) {
304
- out[key] = isFieldBuilder(value) ? value.finalize(key) : value;
342
+ out[key] = isFieldBuilder(value) ? value.finalize(key) : (value as SchemaColumnDecl);
305
343
  }
306
344
  return out;
307
345
  }
@@ -333,14 +371,34 @@ export function schemaTableSqlName(table: SchemaTableDecl): string | undefined {
333
371
  return undefined;
334
372
  }
335
373
 
374
+ /**
375
+ * JS value type for a finalized column (`string` / `number`, plus `| null`
376
+ * when the column is nullable).
377
+ */
378
+ export type InferColumnJs<C> =
379
+ C extends SchemaColumnDecl<infer D, infer N> ? (N extends true ? D : D | null) : unknown;
380
+
381
+ /**
382
+ * Finalize a {@link schemaTable} column input into a typed decl.
383
+ */
384
+ export type FinalizeColumn<T> =
385
+ T extends FieldBuilder<infer D, infer N>
386
+ ? SchemaColumnDecl<D, N>
387
+ : T extends SchemaColumnDecl<infer D, infer N>
388
+ ? SchemaColumnDecl<D, N>
389
+ : SchemaColumnDecl;
390
+
336
391
  /**
337
392
  * Table decl with columns as own properties (`links.code`) for FK / relations.
393
+ *
394
+ * `$inferSelect` is type-level only (Drizzle's own alias) — not set at runtime.
338
395
  */
339
396
  export type SchemaTableWithColumns<
340
397
  C extends Record<string, SchemaColumnDecl> = Record<string, SchemaColumnDecl>,
341
398
  > = Omit<SchemaTableDecl, "columns"> & {
342
399
  readonly columns: Readonly<C>;
343
- } & Omit<C, "kind" | "tableName">;
400
+ readonly $inferSelect: { [K in keyof C]: InferColumnJs<C[K]> };
401
+ } & Omit<C, "kind" | "tableName" | "$inferSelect">;
344
402
 
345
403
  /**
346
404
  * Declare an abstract schema table (ORM-agnostic).
@@ -356,7 +414,7 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
356
414
  name: string,
357
415
  columns: C,
358
416
  extras: readonly SchemaTableExtra[] = [],
359
- ): SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }> {
417
+ ): SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }> {
360
418
  const finalized = finalizeColumnMap(columns);
361
419
  const stamped: Record<string, SchemaColumnDecl> = {};
362
420
  for (const [key, col] of Object.entries(finalized)) {
@@ -377,7 +435,7 @@ export function schemaTable<C extends Record<string, SchemaColumnInput>>(
377
435
  // the table discriminant / SQL name).
378
436
  Object.defineProperty(table, "kind", { value: "schema-table", enumerable: false });
379
437
  Object.defineProperty(table, "tableName", { value: name, enumerable: false });
380
- return table as SchemaTableWithColumns<{ [K in keyof C]: SchemaColumnDecl }>;
438
+ return table as SchemaTableWithColumns<{ [K in keyof C]: FinalizeColumn<C[K]> }>;
381
439
  }
382
440
 
383
441
  /**
@@ -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
+ });
@@ -34,6 +34,15 @@ import {
34
34
 
35
35
  export type { WhereMap } from "./sql-condition.ts";
36
36
 
37
+ /**
38
+ * Inferred select-row shape for a table handle.
39
+ *
40
+ * Uses `$inferSelect` when present (OKE `store.schema.table()` and Drizzle
41
+ * `pgTable()` / `sqliteTable()`). Bare {@link TableHandle} / `unknown` stay
42
+ * {@link SqlRow}.
43
+ */
44
+ export type InferSelectRow<T> = T extends { readonly $inferSelect: infer R } ? R : SqlRow;
45
+
37
46
  /** Serialize stamp frames on a shared connection so concurrent identities cannot interleave. */
38
47
  const rlsStampTails = new WeakMap<SqlConnection, Promise<unknown>>();
39
48
 
@@ -97,44 +106,44 @@ export interface SqlSessionOptions {
97
106
  * Continuations after `.orderBy(...)` — awaitable, optional `.limit` /
98
107
  * `.offset`.
99
108
  */
100
- export interface SelectOrderBuilder extends PromiseLike<SqlRow[]> {
109
+ export interface SelectOrderBuilder<TRow = SqlRow> extends PromiseLike<TRow[]> {
101
110
  /**
102
111
  * Cap the number of returned rows.
103
112
  *
104
113
  * @param n - Max rows
105
114
  */
106
- limit(n: number): Promise<SqlRow[]>;
115
+ limit(n: number): Promise<TRow[]>;
107
116
  /**
108
117
  * Skip the first `n` rows (`LIMIT … OFFSET …`).
109
118
  *
110
119
  * @param n - Rows to skip
111
120
  */
112
- offset(n: number): Promise<SqlRow[]>;
121
+ offset(n: number): Promise<TRow[]>;
113
122
  }
114
123
 
115
124
  /**
116
125
  * Continuations after `.where(...)` on a select — awaitable, optional
117
126
  * `.orderBy(...)` / `.limit(...)` / `.offset(...)`.
118
127
  */
119
- export interface SelectWhereBuilder extends PromiseLike<SqlRow[]> {
128
+ export interface SelectWhereBuilder<TRow = SqlRow> extends PromiseLike<TRow[]> {
120
129
  /**
121
130
  * Order rows with Drizzle `asc()` / `desc()` terms.
122
131
  *
123
132
  * @param orders - Order terms
124
133
  */
125
- orderBy(...orders: readonly unknown[]): SelectOrderBuilder;
134
+ orderBy(...orders: readonly unknown[]): SelectOrderBuilder<TRow>;
126
135
  /**
127
136
  * Cap the number of returned rows.
128
137
  *
129
138
  * @param n - Max rows
130
139
  */
131
- limit(n: number): Promise<SqlRow[]>;
140
+ limit(n: number): Promise<TRow[]>;
132
141
  /**
133
142
  * Skip the first `n` rows.
134
143
  *
135
144
  * @param n - Rows to skip
136
145
  */
137
- offset(n: number): Promise<SqlRow[]>;
146
+ offset(n: number): Promise<TRow[]>;
138
147
  }
139
148
 
140
149
  /**
@@ -142,42 +151,49 @@ export interface SelectWhereBuilder extends PromiseLike<SqlRow[]> {
142
151
  * `.where(...)` / `.orderBy(...)` / `.limit(...)` / `.offset(...)` in any
143
152
  * order.
144
153
  */
145
- export interface SelectFromBuilder extends PromiseLike<SqlRow[]> {
154
+ export interface SelectFromBuilder<TRow = SqlRow> extends PromiseLike<TRow[]> {
146
155
  /**
147
156
  * Filter with a plain equality map or a Drizzle SQL condition
148
157
  * (`eq`, `and`, `or`, `lt`, `like`, …).
149
158
  *
150
159
  * @param where - Condition
151
160
  */
152
- where(where: unknown): SelectWhereBuilder;
161
+ where(where: unknown): SelectWhereBuilder<TRow>;
153
162
  /**
154
163
  * Order rows with Drizzle `asc()` / `desc()` terms.
155
164
  *
156
165
  * @param orders - Order terms
157
166
  */
158
- orderBy(...orders: readonly unknown[]): SelectOrderBuilder;
167
+ orderBy(...orders: readonly unknown[]): SelectOrderBuilder<TRow>;
159
168
  /**
160
169
  * Cap the number of returned rows.
161
170
  *
162
171
  * @param n - Max rows
163
172
  */
164
- limit(n: number): Promise<SqlRow[]>;
173
+ limit(n: number): Promise<TRow[]>;
165
174
  /**
166
175
  * Skip the first `n` rows.
167
176
  *
168
177
  * @param n - Rows to skip
169
178
  */
170
- offset(n: number): Promise<SqlRow[]>;
179
+ offset(n: number): Promise<TRow[]>;
171
180
  }
172
181
 
173
- /** Fluent select builder. */
174
- export interface SelectBuilder {
182
+ /**
183
+ * Fluent select builder.
184
+ *
185
+ * `TLocked` is `undefined` for `select()` (row type inferred from `.from`)
186
+ * and {@link SqlRow} for `select({ alias: col })` projections.
187
+ */
188
+ export interface SelectBuilder<TLocked extends SqlRow | undefined = undefined> {
175
189
  /**
176
190
  * Choose the source table.
177
191
  *
178
192
  * @param table - Table handle or Drizzle table
179
193
  */
180
- from(table: TableHandle | unknown): SelectFromBuilder;
194
+ from<TTable>(
195
+ table: TTable,
196
+ ): SelectFromBuilder<TLocked extends undefined ? InferSelectRow<TTable> : TLocked>;
181
197
  }
182
198
 
183
199
  /** Fluent insert builder. */
@@ -244,12 +260,13 @@ export interface SqlStoreHandle {
244
260
  /** Underlying driver id. */
245
261
  readonly driverId: SqlConnection["driverId"];
246
262
  /**
247
- * Start a select. Optional column map projects / aliases columns
248
- * (`select({ clicks: links.clicks })`).
263
+ * Start a select. No-arg form infers the row from `.from(table)`.
264
+ * A column map (`select({ clicks: links.clicks })`) stays {@link SqlRow}.
249
265
  *
250
266
  * @param columns - Optional `{ alias: drizzleColumn }` map
251
267
  */
252
- select(columns?: unknown): SelectBuilder;
268
+ select(): SelectBuilder<undefined>;
269
+ select(columns: unknown): SelectBuilder<SqlRow>;
253
270
  /**
254
271
  * Start an insert into `table`.
255
272
  *
@@ -600,20 +617,20 @@ export function createSqlStoreHandle(
600
617
  };
601
618
  }
602
619
 
603
- const handle: SqlStoreHandle = {
620
+ const handle = {
604
621
  ref,
605
622
  routedRole: options.routedRole,
606
623
  driverId: connection.driverId,
607
624
 
608
- select(columns?) {
625
+ select(columns?: unknown) {
609
626
  return {
610
- from(table) {
627
+ from(table: TableHandle | unknown) {
611
628
  return selectFrom(table, columns);
612
629
  },
613
630
  };
614
631
  },
615
632
 
616
- insert(table): InsertBuilder {
633
+ insert(table: TableHandle | unknown): InsertBuilder {
617
634
  const name = resolveTableName(table);
618
635
  return {
619
636
  values(row) {
@@ -648,7 +665,7 @@ export function createSqlStoreHandle(
648
665
  };
649
666
  },
650
667
 
651
- update(table): UpdateBuilder {
668
+ update(table: TableHandle | unknown): UpdateBuilder {
652
669
  const name = resolveTableName(table);
653
670
  return {
654
671
  set(row) {
@@ -675,7 +692,7 @@ export function createSqlStoreHandle(
675
692
  };
676
693
  },
677
694
 
678
- async findById(table, idValue) {
695
+ async findById(table: TableHandle | unknown, idValue: string) {
679
696
  await ensureFromMeta(table);
680
697
  const name = resolveTableName(table);
681
698
  const pk = resolvePkColumn(table);
@@ -716,7 +733,7 @@ export function createSqlStoreHandle(
716
733
  return builder;
717
734
  }) as SqlStoreHandle["delete"],
718
735
 
719
- async exists(table, idOrWhere) {
736
+ async exists(table: TableHandle | unknown, idOrWhere: string | WhereMap) {
720
737
  await ensureFromMeta(table);
721
738
  const name = resolveTableName(table);
722
739
  const where: WhereMap =
@@ -734,7 +751,12 @@ export function createSqlStoreHandle(
734
751
  return rows.length > 0;
735
752
  },
736
753
 
737
- async upsert(table, matchOn, values, upsertOptions) {
754
+ async upsert(
755
+ table: TableHandle | unknown,
756
+ matchOn: WhereMap | unknown,
757
+ values: SqlRow,
758
+ upsertOptions?: { readonly onExisting?: "update" },
759
+ ) {
738
760
  await ensureFromMeta(table);
739
761
  const name = resolveTableName(table);
740
762
  const compiled = compileTableWhere(table, matchOn);
@@ -766,7 +788,7 @@ export function createSqlStoreHandle(
766
788
  return { status: "changed" as const };
767
789
  },
768
790
 
769
- async increment(table, idValue, column, by = 1) {
791
+ async increment(table: TableHandle | unknown, idValue: string, column: string, by = 1) {
770
792
  await ensureFromMeta(table);
771
793
  const name = resolveTableName(table);
772
794
  const pk = resolvePkColumn(table);
@@ -785,7 +807,7 @@ export function createSqlStoreHandle(
785
807
  return asNumber(row[sqlCol], sqlCol);
786
808
  },
787
809
 
788
- async raw(sql, params = []) {
810
+ async raw(sql: string, params: readonly unknown[] = []) {
789
811
  const table = tableFromSql(sql);
790
812
  // Memory SQL (console-next seed) implements DML on `exec`, not `query`.
791
813
  // SELECT / RETURNING still go through `query` so callers get rows.
@@ -797,7 +819,7 @@ export function createSqlStoreHandle(
797
819
  return mask(rows, table);
798
820
  },
799
821
 
800
- async count(table, where) {
822
+ async count(table: TableHandle | unknown, where?: unknown) {
801
823
  await ensureFromMeta(table);
802
824
  const name = resolveTableName(table);
803
825
  let sql = `SELECT COUNT(*) AS "count" FROM ${quoteIdent(name)}`;
@@ -813,7 +835,7 @@ export function createSqlStoreHandle(
813
835
  return asNumber(rows[0]?.count ?? rows[0]?.COUNT ?? 0, "count");
814
836
  },
815
837
 
816
- async page(table, options) {
838
+ async page(table: TableHandle | unknown, options: SqlPageOptions) {
817
839
  if (options.after !== undefined && options.before !== undefined) {
818
840
  throw new Error("page(): after and before (keyset) cannot combine");
819
841
  }
@@ -838,7 +860,7 @@ export function createSqlStoreHandle(
838
860
  });
839
861
  },
840
862
 
841
- async ensureTable(table) {
863
+ async ensureTable(table: TableHandle) {
842
864
  const cols = Object.values(table.columns);
843
865
  const pk = resolvePkColumn(table);
844
866
  const intType =
@@ -863,7 +885,7 @@ export function createSqlStoreHandle(
863
885
  },
864
886
  };
865
887
 
866
- return handle;
888
+ return handle as SqlStoreHandle;
867
889
  }
868
890
 
869
891
  /**
@@ -47,6 +47,8 @@ export type {
47
47
  SchemaColumnInput,
48
48
  SchemaTableDecl,
49
49
  SchemaTableWithColumns,
50
+ InferColumnJs,
51
+ FinalizeColumn,
50
52
  SchemaRelationsDecl,
51
53
  SchemaPolicyDecl,
52
54
  SchemaTableExtra,
@@ -121,6 +123,7 @@ export type {
121
123
  SelectFromBuilder,
122
124
  SelectWhereBuilder,
123
125
  SelectOrderBuilder,
126
+ InferSelectRow,
124
127
  InsertBuilder,
125
128
  InsertValuesBuilder,
126
129
  SqlSessionOptions,
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ export {
62
62
  type StoreDecl,
63
63
  type StoreRuntime,
64
64
  type SqlStoreHandle,
65
+ type InferSelectRow,
65
66
  type TableHandle,
66
67
  type SchemaTableDecl,
67
68
  type SchemaColumnDecl,