@oakbun/ws 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/index.d.ts +1286 -0
  2. package/package.json +4 -4
  3. package/dist/core/src/adapter/mysql.d.ts +0 -21
  4. package/dist/core/src/adapter/mysql.d.ts.map +0 -1
  5. package/dist/core/src/adapter/postgres.d.ts +0 -16
  6. package/dist/core/src/adapter/postgres.d.ts.map +0 -1
  7. package/dist/core/src/adapter/resolve.d.ts +0 -16
  8. package/dist/core/src/adapter/resolve.d.ts.map +0 -1
  9. package/dist/core/src/adapter/sqlite.d.ts +0 -17
  10. package/dist/core/src/adapter/sqlite.d.ts.map +0 -1
  11. package/dist/core/src/adapter/types.d.ts +0 -29
  12. package/dist/core/src/adapter/types.d.ts.map +0 -1
  13. package/dist/core/src/app/audit-wiring.d.ts +0 -6
  14. package/dist/core/src/app/audit-wiring.d.ts.map +0 -1
  15. package/dist/core/src/app/body-size-limit.d.ts +0 -29
  16. package/dist/core/src/app/body-size-limit.d.ts.map +0 -1
  17. package/dist/core/src/app/compression.d.ts +0 -25
  18. package/dist/core/src/app/compression.d.ts.map +0 -1
  19. package/dist/core/src/app/cookies.d.ts +0 -17
  20. package/dist/core/src/app/cookies.d.ts.map +0 -1
  21. package/dist/core/src/app/cors.d.ts +0 -65
  22. package/dist/core/src/app/cors.d.ts.map +0 -1
  23. package/dist/core/src/app/csrf.d.ts +0 -52
  24. package/dist/core/src/app/csrf.d.ts.map +0 -1
  25. package/dist/core/src/app/health.d.ts +0 -36
  26. package/dist/core/src/app/health.d.ts.map +0 -1
  27. package/dist/core/src/app/index.d.ts +0 -282
  28. package/dist/core/src/app/index.d.ts.map +0 -1
  29. package/dist/core/src/app/logger.d.ts +0 -4
  30. package/dist/core/src/app/logger.d.ts.map +0 -1
  31. package/dist/core/src/app/middleware.d.ts +0 -20
  32. package/dist/core/src/app/middleware.d.ts.map +0 -1
  33. package/dist/core/src/app/module.d.ts +0 -273
  34. package/dist/core/src/app/module.d.ts.map +0 -1
  35. package/dist/core/src/app/plugin.d.ts +0 -112
  36. package/dist/core/src/app/plugin.d.ts.map +0 -1
  37. package/dist/core/src/app/rate-limit.d.ts +0 -76
  38. package/dist/core/src/app/rate-limit.d.ts.map +0 -1
  39. package/dist/core/src/app/request-id.d.ts +0 -52
  40. package/dist/core/src/app/request-id.d.ts.map +0 -1
  41. package/dist/core/src/app/router.d.ts +0 -6
  42. package/dist/core/src/app/router.d.ts.map +0 -1
  43. package/dist/core/src/app/scalar.d.ts +0 -40
  44. package/dist/core/src/app/scalar.d.ts.map +0 -1
  45. package/dist/core/src/app/secure-headers.d.ts +0 -48
  46. package/dist/core/src/app/secure-headers.d.ts.map +0 -1
  47. package/dist/core/src/app/system-ctx.d.ts +0 -3
  48. package/dist/core/src/app/system-ctx.d.ts.map +0 -1
  49. package/dist/core/src/app/types.d.ts +0 -277
  50. package/dist/core/src/app/types.d.ts.map +0 -1
  51. package/dist/core/src/cli/config/types.d.ts +0 -31
  52. package/dist/core/src/cli/config/types.d.ts.map +0 -1
  53. package/dist/core/src/cli/index.d.ts +0 -3
  54. package/dist/core/src/cli/index.d.ts.map +0 -1
  55. package/dist/core/src/client/error.d.ts +0 -8
  56. package/dist/core/src/client/error.d.ts.map +0 -1
  57. package/dist/core/src/client/index.d.ts +0 -33
  58. package/dist/core/src/client/index.d.ts.map +0 -1
  59. package/dist/core/src/client/proxy.d.ts +0 -88
  60. package/dist/core/src/client/proxy.d.ts.map +0 -1
  61. package/dist/core/src/client/test-client.d.ts +0 -30
  62. package/dist/core/src/client/test-client.d.ts.map +0 -1
  63. package/dist/core/src/cron/index.d.ts +0 -73
  64. package/dist/core/src/cron/index.d.ts.map +0 -1
  65. package/dist/core/src/db/index.d.ts +0 -281
  66. package/dist/core/src/db/index.d.ts.map +0 -1
  67. package/dist/core/src/db/migrations/diff.d.ts +0 -5
  68. package/dist/core/src/db/migrations/diff.d.ts.map +0 -1
  69. package/dist/core/src/db/migrations/generator.d.ts +0 -26
  70. package/dist/core/src/db/migrations/generator.d.ts.map +0 -1
  71. package/dist/core/src/db/migrations/index.d.ts +0 -9
  72. package/dist/core/src/db/migrations/index.d.ts.map +0 -1
  73. package/dist/core/src/db/migrations/introspect.d.ts +0 -8
  74. package/dist/core/src/db/migrations/introspect.d.ts.map +0 -1
  75. package/dist/core/src/db/migrations/migrator.d.ts +0 -10
  76. package/dist/core/src/db/migrations/migrator.d.ts.map +0 -1
  77. package/dist/core/src/db/migrations/runner.d.ts +0 -7
  78. package/dist/core/src/db/migrations/runner.d.ts.map +0 -1
  79. package/dist/core/src/db/migrations/tracker.d.ts +0 -7
  80. package/dist/core/src/db/migrations/tracker.d.ts.map +0 -1
  81. package/dist/core/src/db/migrations/types.d.ts +0 -70
  82. package/dist/core/src/db/migrations/types.d.ts.map +0 -1
  83. package/dist/core/src/db/sql.d.ts +0 -148
  84. package/dist/core/src/db/sql.d.ts.map +0 -1
  85. package/dist/core/src/errors/index.d.ts +0 -30
  86. package/dist/core/src/errors/index.d.ts.map +0 -1
  87. package/dist/core/src/events/handler.d.ts +0 -42
  88. package/dist/core/src/events/handler.d.ts.map +0 -1
  89. package/dist/core/src/events/index.d.ts +0 -48
  90. package/dist/core/src/events/index.d.ts.map +0 -1
  91. package/dist/core/src/hooks/executor.d.ts +0 -20
  92. package/dist/core/src/hooks/executor.d.ts.map +0 -1
  93. package/dist/core/src/hooks/types.d.ts +0 -10
  94. package/dist/core/src/hooks/types.d.ts.map +0 -1
  95. package/dist/core/src/index.d.ts +0 -83
  96. package/dist/core/src/index.d.ts.map +0 -1
  97. package/dist/core/src/model/index.d.ts +0 -24
  98. package/dist/core/src/model/index.d.ts.map +0 -1
  99. package/dist/core/src/openapi/generator.d.ts +0 -58
  100. package/dist/core/src/openapi/generator.d.ts.map +0 -1
  101. package/dist/core/src/openapi/zod-to-schema.d.ts +0 -4
  102. package/dist/core/src/openapi/zod-to-schema.d.ts.map +0 -1
  103. package/dist/core/src/resource/errors.d.ts +0 -2
  104. package/dist/core/src/resource/errors.d.ts.map +0 -1
  105. package/dist/core/src/resource/index.d.ts +0 -98
  106. package/dist/core/src/resource/index.d.ts.map +0 -1
  107. package/dist/core/src/resource/zod-table.d.ts +0 -6
  108. package/dist/core/src/resource/zod-table.d.ts.map +0 -1
  109. package/dist/core/src/schema/audit.d.ts +0 -25
  110. package/dist/core/src/schema/audit.d.ts.map +0 -1
  111. package/dist/core/src/schema/column.d.ts +0 -31
  112. package/dist/core/src/schema/column.d.ts.map +0 -1
  113. package/dist/core/src/schema/table.d.ts +0 -68
  114. package/dist/core/src/schema/table.d.ts.map +0 -1
  115. package/dist/core/src/service/index.d.ts +0 -27
  116. package/dist/core/src/service/index.d.ts.map +0 -1
  117. package/dist/ws/src/adapter.d.ts +0 -56
  118. package/dist/ws/src/adapter.d.ts.map +0 -1
  119. package/dist/ws/src/index.d.ts +0 -5
  120. package/dist/ws/src/index.d.ts.map +0 -1
  121. package/dist/ws/src/module-augment.d.ts +0 -7
  122. package/dist/ws/src/module-augment.d.ts.map +0 -1
  123. package/dist/ws/src/types.d.ts +0 -87
  124. package/dist/ws/src/types.d.ts.map +0 -1
@@ -0,0 +1,1286 @@
1
+ import * as bun from 'bun';
2
+ import { ZodTypeAny } from 'zod';
3
+ import * as oakbun from 'oakbun';
4
+ import { AuthPayload, VelnWsAdapter, WsRouteShape as WsRouteShape$1, BaseCtx as BaseCtx$1 } from 'oakbun';
5
+
6
+ type BindingValue = string | number | bigint | boolean | null | Uint8Array;
7
+ interface ExecuteResult {
8
+ rowsAffected: number;
9
+ lastInsertId?: number | bigint;
10
+ }
11
+ /** Emitted by the adapter after every query() or execute() call. */
12
+ interface QueryLogEntry {
13
+ /** The SQL string that was executed. */
14
+ sql: string;
15
+ /** The bound parameter values. */
16
+ params: BindingValue[];
17
+ /** Wall-clock duration in milliseconds (performance.now() resolution). */
18
+ durationMs: number;
19
+ /** Whether the call was query() (returns rows) or execute() (DML/DDL). */
20
+ type: 'query' | 'execute';
21
+ }
22
+ interface VelnAdapter {
23
+ query<T = Record<string, unknown>>(sql: string, params?: BindingValue[]): Promise<T[]>;
24
+ execute(sql: string, params?: BindingValue[]): Promise<ExecuteResult>;
25
+ transaction<T>(fn: (tx: VelnAdapter) => Promise<T>): Promise<T>;
26
+ close(): Promise<void>;
27
+ /**
28
+ * Optional query observer. When set, the adapter calls this after every
29
+ * query() and execute() with timing and SQL details.
30
+ * Set by dbPlugin when query logging is enabled — do not call directly.
31
+ */
32
+ onQuery?: (entry: QueryLogEntry) => void;
33
+ }
34
+
35
+ type SqlType = 'INTEGER' | 'TEXT' | 'REAL' | 'BOOLEAN' | 'TIMESTAMP' | 'JSON' | 'UUID' | 'BLOB';
36
+ interface ColumnDef {
37
+ type: SqlType;
38
+ nullable: boolean;
39
+ primaryKey: boolean;
40
+ autoIncrement: boolean;
41
+ unique: boolean;
42
+ defaultValue?: unknown;
43
+ defaultFn?: () => unknown;
44
+ }
45
+ declare class Column<T> {
46
+ readonly def: Readonly<ColumnDef>;
47
+ readonly _: T;
48
+ constructor(def: Readonly<ColumnDef>);
49
+ nullable(): Column<T | null>;
50
+ primaryKey(): Column<T>;
51
+ unique(): Column<T>;
52
+ default(value: NonNullable<T>): Column<T>;
53
+ defaultFn(fn: () => NonNullable<T>): Column<T>;
54
+ }
55
+
56
+ type SchemaMap = Record<string, Column<any>>;
57
+ type InferRow<T> = T extends TableDef<infer R, any, any> ? R : T extends SchemaMap ? {
58
+ [K in keyof T]: T[K] extends Column<infer C> ? C : never;
59
+ } : never;
60
+ type IsOptionalOnInsert<C extends Column<any>> = C['def']['primaryKey'] extends true ? true : C['def']['defaultValue'] extends undefined ? C['def']['defaultFn'] extends undefined ? C['def']['nullable'] extends true ? true : false : true : true;
61
+ type InferInsertFromSchema<S extends SchemaMap> = {
62
+ [K in keyof S as IsOptionalOnInsert<S[K]> extends true ? never : K]: S[K] extends Column<infer T> ? NonNullable<T> : never;
63
+ } & {
64
+ [K in keyof S as IsOptionalOnInsert<S[K]> extends true ? K : never]?: S[K] extends Column<infer T> ? T : never;
65
+ };
66
+ type InferInsert<T> = T extends TableDef<any, infer S, any> ? InferInsertFromSchema<S> : T extends SchemaMap ? InferInsertFromSchema<T> : never;
67
+ type InferUpdate<T> = T extends TableDef<infer R, infer S, any> ? Partial<R> & {
68
+ [K in keyof S as S[K] extends Column<any> ? S[K]['def']['primaryKey'] extends true ? K : never : never]: R[K & keyof R];
69
+ } : T extends SchemaMap ? Partial<{
70
+ [K in keyof T]: T[K] extends Column<infer C> ? C : never;
71
+ }> : never;
72
+ interface TableHookHandlers<T> {
73
+ beforeInsert?: (data: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
74
+ afterInsert?: (result: T, input: Partial<T>) => void | Promise<void>;
75
+ beforeUpdate?: (current: T, patch: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
76
+ afterUpdate?: (result: T, before: T) => void | Promise<void>;
77
+ beforeDelete?: (current: T) => void | Promise<void>;
78
+ afterDelete?: (deleted: T) => void | Promise<void>;
79
+ }
80
+ interface TableEventMap {
81
+ afterInsert?: string;
82
+ afterUpdate?: string;
83
+ afterDelete?: string;
84
+ }
85
+ type InferTableEvents<T, M extends TableEventMap> = (M['afterInsert'] extends string ? {
86
+ [_ in M['afterInsert']]: T;
87
+ } : Record<never, never>) & (M['afterUpdate'] extends string ? {
88
+ [_ in M['afterUpdate']]: {
89
+ before: T;
90
+ after: T;
91
+ };
92
+ } : Record<never, never>) & (M['afterDelete'] extends string ? {
93
+ [_ in M['afterDelete']]: T;
94
+ } : Record<never, never>);
95
+ type RelationKind = 'belongsTo' | 'hasMany' | 'manyToMany';
96
+ /**
97
+ * Metadata for a single declared relation.
98
+ * TForeign captures the foreign table's row type so that WithRelations can
99
+ * produce concrete types (User, Post[]) rather than unknown.
100
+ *
101
+ * `getTable` is a lazy getter to allow circular references between tables.
102
+ */
103
+ interface RelationMeta<TForeign = unknown> {
104
+ kind: RelationKind;
105
+ name: string;
106
+ getTable: () => TableDef<TForeign, any, any>;
107
+ /** FK column name — on this table for belongsTo, on foreign table for hasMany */
108
+ foreignKey: string;
109
+ /** manyToMany only */
110
+ pivot?: {
111
+ table: TableDef<any, any, any>;
112
+ localKey: string;
113
+ foreignKey: string;
114
+ };
115
+ }
116
+ /** All declared relations on a table, keyed by relation name. */
117
+ type RelationsMap = Record<string, RelationMeta<any>>;
118
+ interface BelongsToRelation<TForeign> extends RelationMeta<TForeign> {
119
+ kind: 'belongsTo';
120
+ }
121
+ interface HasManyRelation<TForeign> extends RelationMeta<TForeign> {
122
+ kind: 'hasMany';
123
+ }
124
+ /**
125
+ * Derives the result type of a single loaded relation.
126
+ * - belongsTo → TForeign | null
127
+ * - hasMany → TForeign[]
128
+ * - manyToMany → never (not supported in Spec B)
129
+ */
130
+ type InferRelationResult<R> = R extends BelongsToRelation<infer TForeign> ? TForeign | null : R extends HasManyRelation<infer TForeign> ? TForeign[] : never;
131
+ /**
132
+ * Merges a row type T with the requested relations.
133
+ * Keys must be keys of the table's relations map.
134
+ *
135
+ * @example
136
+ * type PostWithAuthor = WithRelations<Post, typeof postsTable, 'author'>
137
+ * // → Post & { author: User | null }
138
+ */
139
+ type WithRelations<T, TTable extends {
140
+ relations: RelationsMap;
141
+ }, Keys extends keyof TTable['relations'] & string> = T & {
142
+ [K in Keys]: InferRelationResult<TTable['relations'][K]>;
143
+ };
144
+ interface TableDef<T, S extends SchemaMap = SchemaMap, TEvents extends TableEventMap = TableEventMap, TRelations extends RelationsMap = RelationsMap> {
145
+ readonly name: string;
146
+ readonly schema: S;
147
+ readonly primaryKey: keyof T & string;
148
+ readonly hooks: TableHookHandlers<T>[];
149
+ readonly events: TEvents;
150
+ readonly _eventMap: InferTableEvents<T, TEvents>;
151
+ /** Declared relations — concrete typed map so WithRelations can infer foreign types. */
152
+ readonly relations: TRelations;
153
+ /** The column used for soft delete, or null if not configured. */
154
+ readonly softDeleteColumn: (keyof T & string) | null;
155
+ }
156
+
157
+ interface ModuleHookHandlers<T, TCtx = unknown> {
158
+ beforeInsert?: (ctx: TCtx, data: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
159
+ afterInsert?: (ctx: TCtx, result: T, input: Partial<T>) => void | Promise<void>;
160
+ beforeUpdate?: (ctx: TCtx, current: T, patch: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
161
+ afterUpdate?: (ctx: TCtx, result: T, before: T) => void | Promise<void>;
162
+ beforeDelete?: (ctx: TCtx, current: T) => void | Promise<void>;
163
+ afterDelete?: (ctx: TCtx, deleted: T) => void | Promise<void>;
164
+ }
165
+
166
+ type EventHandler = (payload: unknown, ctx: unknown) => Promise<void> | void;
167
+ declare class RequestEventQueue {
168
+ private readonly buffer;
169
+ collect(name: string, payload: unknown): void;
170
+ flush(ctx: unknown, bus: EventBusAdapter | InMemoryEventBus): Promise<void>;
171
+ /** Drain — returns collected events and clears the buffer.
172
+ * Used by the TX path to hand events off to TransactionResult. */
173
+ drain(): PendingEvent[];
174
+ /** Number of buffered events — useful in tests. */
175
+ get size(): number;
176
+ }
177
+ interface VelnEvents {
178
+ }
179
+ type EventBusErrorHandler = (event: string, error: unknown) => void;
180
+ interface EventBusOptions {
181
+ /** Called when an event handler throws. Defaults to console.error. */
182
+ onError?: EventBusErrorHandler;
183
+ }
184
+ /**
185
+ * EventBusAdapter — minimal interface for event bus implementations.
186
+ *
187
+ * Default: InMemoryEventBus (single-process, zero latency)
188
+ *
189
+ * For multi-worker deployments: BroadcastChannelAdapter (@oakbun/broadcast, roadmap)
190
+ * For multi-server deployments: RedisAdapter (@oakbun/redis, roadmap)
191
+ *
192
+ * NOTE: EventBus is single-process by default. Events fired on instance A
193
+ * will NOT reach instance B without a distributed adapter.
194
+ */
195
+ interface EventBusAdapter {
196
+ on(event: string, handler: (payload: unknown) => void): void;
197
+ emit(event: string, payload: unknown): Promise<void>;
198
+ }
199
+ declare class InMemoryEventBus {
200
+ private readonly handlers;
201
+ private readonly _onError;
202
+ constructor(options?: EventBusOptions);
203
+ on<K extends keyof VelnEvents>(event: K, handler: (payload: VelnEvents[K], ctx: unknown) => Promise<void> | void): this;
204
+ on(event: string, handler: EventHandler): this;
205
+ _emit(event: string, payload: unknown, ctx: unknown): void;
206
+ emit(event: string, payload: unknown): Promise<void>;
207
+ flush(events: PendingEvent[], ctx: unknown): Promise<void>;
208
+ }
209
+ /** @deprecated Use InMemoryEventBus instead. EventBus will be removed in a future version. */
210
+ declare const EventBus: typeof InMemoryEventBus;
211
+ type EventBus = InMemoryEventBus;
212
+
213
+ declare class HookExecutor {
214
+ private readonly registry;
215
+ private _adapter?;
216
+ constructor();
217
+ setAdapter(adapter: VelnAdapter): void;
218
+ getAdapter(): VelnAdapter | undefined;
219
+ registerModuleHook<T, TCtx>(tableName: string, handlers: ModuleHookHandlers<T, TCtx>): void;
220
+ runBeforeInsert<T>(table: TableDef<T>, ctx: unknown, data: Partial<T>): Promise<Partial<T>>;
221
+ runBeforeUpdate<T>(table: TableDef<T>, ctx: unknown, current: T, patch: Partial<T>): Promise<Partial<T>>;
222
+ runBeforeDelete<T>(table: TableDef<T>, ctx: unknown, current: T): Promise<void>;
223
+ runAfterInsert<T>(table: TableDef<T>, ctx: unknown, result: T, input: Partial<T>, queue?: RequestEventQueue): Promise<void>;
224
+ runAfterUpdate<T>(table: TableDef<T>, ctx: unknown, result: T, before: T, queue?: RequestEventQueue): Promise<void>;
225
+ runAfterDelete<T>(table: TableDef<T>, ctx: unknown, deleted: T, queue?: RequestEventQueue): Promise<void>;
226
+ private _moduleHandlers;
227
+ }
228
+
229
+ /**
230
+ * Phantom-typed wrapper for a subquery SQL fragment.
231
+ * Col and T are used only at compile time — _phantom is never read at runtime.
232
+ *
233
+ * Produced by SelectBuilder.columns(col).subquery().
234
+ * Accepted by WhereOp IN / NOT IN in place of an array.
235
+ */
236
+ interface SubqueryResult<Col extends string, T> {
237
+ readonly _sql: string;
238
+ readonly _params: BindingValue[];
239
+ readonly _phantom: {
240
+ col: Col;
241
+ type: T;
242
+ };
243
+ }
244
+ interface JoinClause {
245
+ type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
246
+ table: string;
247
+ on: string;
248
+ }
249
+ /** SQL dialect — used for ILIKE fallback on non-Postgres adapters. */
250
+ type SqlDialect = 'sqlite' | 'postgres' | 'mysql';
251
+ /** Accepted value for IN / NOT IN — either a plain array or a typed subquery. */
252
+ type InValue<T> = T[] | SubqueryResult<string, T>;
253
+ /** Explicit operator condition for a single column. */
254
+ type WhereOp<T> = {
255
+ op: '=';
256
+ value: T;
257
+ } | {
258
+ op: '!=';
259
+ value: T;
260
+ } | {
261
+ op: '>';
262
+ value: T;
263
+ } | {
264
+ op: '>=';
265
+ value: T;
266
+ } | {
267
+ op: '<';
268
+ value: T;
269
+ } | {
270
+ op: '<=';
271
+ value: T;
272
+ } | {
273
+ op: 'IN';
274
+ value: InValue<T>;
275
+ } | {
276
+ op: 'NOT IN';
277
+ value: InValue<T>;
278
+ } | {
279
+ op: 'LIKE';
280
+ value: string;
281
+ } | {
282
+ op: 'ILIKE';
283
+ value: string;
284
+ } | {
285
+ op: 'IS NULL';
286
+ } | {
287
+ op: 'IS NOT NULL';
288
+ };
289
+ /** Per-field condition — shorthand (plain value = equality) or explicit operator. */
290
+ type FieldCondition<T> = T | WhereOp<T>;
291
+ /** Map of column conditions — each field is optional. */
292
+ type WhereConditions<TRow> = {
293
+ [K in keyof TRow]?: FieldCondition<TRow[K]>;
294
+ };
295
+ /**
296
+ * Full WHERE input — either a flat conditions map, OR-group, or AND-group.
297
+ * OR/AND values are recursively WhereInput allowing nesting.
298
+ */
299
+ type WhereInput<TRow> = WhereConditions<TRow> | {
300
+ OR: WhereInput<TRow>[];
301
+ } | {
302
+ AND: WhereInput<TRow>[];
303
+ };
304
+ /** A single aggregate expression: FN("col") AS alias. */
305
+ interface AggregateClause {
306
+ alias: string;
307
+ fn: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
308
+ col?: string;
309
+ }
310
+ interface SelectOptions {
311
+ limit?: number;
312
+ offset?: number;
313
+ orderBy?: {
314
+ col: string;
315
+ dir: 'ASC' | 'DESC';
316
+ }[];
317
+ columns?: string[];
318
+ groupBy?: string[];
319
+ aggregates?: AggregateClause[];
320
+ having?: WhereInput<Record<string, unknown>>;
321
+ distinct?: boolean;
322
+ }
323
+
324
+ interface QueryLog {
325
+ /** Total number of queries executed during this request. */
326
+ queries: number;
327
+ /** Cumulative wall-clock duration of all queries in ms. */
328
+ totalMs: number;
329
+ /** Individual query entries — populated only when logQueries is true. */
330
+ entries: QueryLogEntry[];
331
+ /** Warning threshold — exceeded → N+1 warning. */
332
+ threshold: number;
333
+ /** Whether individual query entries should be captured (for logQueries). */
334
+ logQueries: boolean;
335
+ }
336
+ interface PendingEvent {
337
+ name: string;
338
+ payload: unknown;
339
+ }
340
+ interface TransactionResult<T> {
341
+ result: T;
342
+ events: PendingEvent[];
343
+ }
344
+ declare class BoundVelnDB {
345
+ private readonly hooks;
346
+ private readonly ctx;
347
+ private readonly queue?;
348
+ private readonly dialect;
349
+ /** Per-request query counter — incremented for every query() and execute() call on this instance. */
350
+ _queryCount: number;
351
+ private readonly adapter;
352
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue?: RequestEventQueue | undefined, queryLog?: QueryLog, dialect?: SqlDialect);
353
+ from<T, S extends SchemaMap, TRelations extends RelationsMap>(table: TableDef<T, S, any, TRelations>): SelectBuilder<T, S, TRelations>;
354
+ /**
355
+ * Start a JOIN query from the given table name.
356
+ * Returns a JoinBuilder — call .join()/.leftJoin() etc. to add clauses,
357
+ * then .select() to execute and get Record<string, unknown>[] results.
358
+ *
359
+ * @example
360
+ * const rows = await db.join('orders')
361
+ * .columns(['orders.id', 'users.name'])
362
+ * .join('users', 'orders.user_id = users.id')
363
+ * .where('orders.status = ?', ['pending'])
364
+ * .select()
365
+ */
366
+ join(tableName: string): JoinBuilder;
367
+ into<T, S extends SchemaMap>(table: TableDef<T, S>): InsertBuilder<T, S>;
368
+ /**
369
+ * DataLoader-pattern relation fetch — single IN-query, no N+1.
370
+ * Returns a Map keyed by the foreign-key value; each entry is an array of
371
+ * matching child rows (for one-to-many relations).
372
+ *
373
+ * Two call forms:
374
+ *
375
+ * @example — explicit (original)
376
+ * const authorMap = await db.loadRelation(posts, 'authorId', usersTable, 'id')
377
+ *
378
+ * @example — name-based (reads relation metadata declared on the table)
379
+ * const authorMap = await db.loadRelation(posts, 'author', postsTable)
380
+ */
381
+ loadRelation<TParent extends Record<string, unknown>, TChild, TFk extends keyof TParent & string, TPk extends keyof TChild & string>(parents: TParent[], foreignKey: TFk, childTable: TableDef<TChild>, primaryKey: TPk): Promise<Map<TParent[TFk], TChild[]>>;
382
+ loadRelation<TParent extends Record<string, unknown>>(parents: TParent[], relationName: string, sourceTable: TableDef<any>): Promise<Map<unknown, unknown>>;
383
+ /**
384
+ * Convenience variant of loadRelation for belongs-to (many-to-one) relations.
385
+ * Returns Map<fkValue, TChild> — single child per key instead of an array.
386
+ *
387
+ * Two call forms:
388
+ *
389
+ * @example — explicit (original)
390
+ * const authorMap = await db.loadRelationOne(posts, 'authorId', usersTable, 'id')
391
+ *
392
+ * @example — name-based
393
+ * const authorMap = await db.loadRelationOne(posts, 'author', postsTable)
394
+ */
395
+ loadRelationOne<TParent extends Record<string, unknown>, TChild, TFk extends keyof TParent & string, TPk extends keyof TChild & string>(parents: TParent[], foreignKey: TFk, childTable: TableDef<TChild>, primaryKey: TPk): Promise<Map<TParent[TFk], TChild>>;
396
+ loadRelationOne<TParent extends Record<string, unknown>>(parents: TParent[], relationName: string, sourceTable: TableDef<any>): Promise<Map<unknown, unknown>>;
397
+ /**
398
+ * Shared implementation for name-based loadRelation / loadRelationOne.
399
+ * Reads RelationMeta from sourceTable.relations, validates the kind,
400
+ * and delegates to the explicit overload.
401
+ */
402
+ private _loadRelationByName;
403
+ transaction<T>(fn: (db: BoundVelnDB) => Promise<T>): Promise<TransactionResult<T>>;
404
+ /**
405
+ * Execute raw SQL and return typed rows.
406
+ *
407
+ * Without a schema the return type is `Record<string, unknown>[]`.
408
+ * With a schema (e.g. a Zod object) every row is validated at runtime
409
+ * and the return type is inferred from the schema.
410
+ *
411
+ * @example
412
+ * // Untyped
413
+ * const rows = await ctx.db.raw('SELECT * FROM orders WHERE amount > ?', [100])
414
+ *
415
+ * // Typed + validated
416
+ * const schema = z.object({ id: z.number(), amount: z.number() })
417
+ * const rows = await ctx.db.raw('SELECT id, amount FROM orders WHERE amount > ?', [100], schema)
418
+ */
419
+ raw<T = Record<string, unknown>>(sql: string, params?: BindingValue[], schema?: {
420
+ parse: (row: unknown) => T;
421
+ }): Promise<T[]>;
422
+ }
423
+ declare class SelectBuilder<T, S extends SchemaMap, TRelations extends RelationsMap = RelationsMap> {
424
+ private readonly adapter;
425
+ private readonly hooks;
426
+ private readonly ctx;
427
+ private readonly queue;
428
+ private readonly table;
429
+ private readonly conditions;
430
+ private readonly _options;
431
+ private readonly _rawWhere;
432
+ private readonly _dialect;
433
+ private readonly _withRelations;
434
+ private readonly _includeDeleted;
435
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S, any, TRelations>, conditions: WhereInput<T>, _options?: SelectOptions, _rawWhere?: {
436
+ sql: string;
437
+ params: BindingValue[];
438
+ }[], _dialect?: SqlDialect, _withRelations?: string[], _includeDeleted?: boolean);
439
+ private _cloneWith;
440
+ private _clone;
441
+ /**
442
+ * Eager-load relations alongside the main query.
443
+ * One additional IN-query per relation — never N+1 regardless of row count.
444
+ *
445
+ * @example
446
+ * const posts = await db.from(postsTable).with({ author: true }).select()
447
+ * posts[0].author // → User | null (fully typed)
448
+ * posts[0].title // → string (original fields preserved)
449
+ */
450
+ with<Keys extends keyof TRelations & string>(relations: {
451
+ [K in Keys]: true;
452
+ }): SelectBuilder<WithRelations<T, {
453
+ relations: TRelations;
454
+ }, Keys>, S, TRelations>;
455
+ /**
456
+ * Include soft-deleted rows in the query result.
457
+ * By default, tables with `.withSoftDelete()` automatically exclude rows
458
+ * where the soft-delete column is not null.
459
+ *
460
+ * Has no effect on tables without soft delete configured.
461
+ *
462
+ * @example
463
+ * const allUsers = await db.from(usersTable).withDeleted().select()
464
+ */
465
+ withDeleted(): SelectBuilder<T, S, TRelations>;
466
+ /**
467
+ * Add WHERE conditions. Accepts:
468
+ * - Plain equality map: `.where({ role: 'admin' })`
469
+ * - Operator condition: `.where({ age: { op: '>=', value: 18 } })`
470
+ * - OR group: `.where({ OR: [{ role: 'admin' }, { role: 'mod' }] })`
471
+ * - AND group: `.where({ AND: [...] })`
472
+ *
473
+ * Multiple `.where()` calls are combined with AND.
474
+ */
475
+ where(conditions: WhereInput<T>): SelectBuilder<T, S, TRelations>;
476
+ /**
477
+ * Append a raw SQL WHERE fragment, combined with AND.
478
+ * Use for conditions the builder cannot express.
479
+ *
480
+ * @example
481
+ * .whereRaw('"score" > "threshold"', [])
482
+ * .whereRaw('"created_at" > ?', ['2024-01-01'])
483
+ */
484
+ whereRaw(sql: string, params: BindingValue[]): SelectBuilder<T, S, TRelations>;
485
+ /**
486
+ * Apply SELECT DISTINCT — deduplicate rows in the result set.
487
+ * Combine with `.columns()` to deduplicate on specific columns.
488
+ *
489
+ * @example
490
+ * await db.from(usersTable).columns('name').distinct().select()
491
+ * // → SELECT DISTINCT "name" FROM "users"
492
+ */
493
+ distinct(): SelectBuilder<T, S, TRelations>;
494
+ /** Limit the number of rows returned. Bound as a parameter — never interpolated. */
495
+ limit(n: number): SelectBuilder<T, S, TRelations>;
496
+ /** Skip the first n rows. Bound as a parameter — never interpolated. */
497
+ offset(n: number): SelectBuilder<T, S, TRelations>;
498
+ /** Add an ORDER BY clause. Multiple calls accumulate in order. */
499
+ orderBy(col: keyof T & string, dir?: 'ASC' | 'DESC'): SelectBuilder<T, S, TRelations>;
500
+ /**
501
+ * Convenience helper for cursor-based pagination.
502
+ * page(1, 10) → LIMIT 10 OFFSET 0
503
+ * page(2, 10) → LIMIT 10 OFFSET 10
504
+ */
505
+ page(page: number, size: number): SelectBuilder<T, S, TRelations>;
506
+ /**
507
+ * Restrict which columns are returned.
508
+ *
509
+ * Single-column form returns a ColumnRestrictedBuilder, enabling .subquery():
510
+ * db.from(usersTable).columns('id').subquery() // → SubqueryResult<'id', number>
511
+ *
512
+ * Multi-column form returns a narrowed SelectBuilder:
513
+ * db.from(usersTable).columns('id', 'name') // → SelectBuilder<Pick<User, 'id'|'name'>, ...>
514
+ */
515
+ columns<K extends keyof T & string>(col: K): ColumnRestrictedBuilder<K, T[K], S, TRelations>;
516
+ columns<K extends keyof T & string>(...cols: K[]): SelectBuilder<Pick<T, K>, S, TRelations>;
517
+ /**
518
+ * Build SELECT SQL + params without executing the query.
519
+ * Used internally by ColumnRestrictedBuilder.subquery().
520
+ */
521
+ /** Internal accessor for ColumnRestrictedBuilder / UnionBuilder — returns the adapter. */
522
+ _getAdapter(): VelnAdapter;
523
+ /** Internal accessor for ColumnRestrictedBuilder / UnionBuilder — returns the SQL dialect. */
524
+ _getDialect(): SqlDialect;
525
+ /**
526
+ * Returns the effective WHERE conditions, injecting the soft-delete IS NULL
527
+ * filter when the table has a soft-delete column and .withDeleted() was not called.
528
+ */
529
+ private _effectiveConditions;
530
+ _buildSelectSQL(): {
531
+ sql: string;
532
+ params: BindingValue[];
533
+ };
534
+ /**
535
+ * Add a GROUP BY clause. Multiple columns are comma-separated.
536
+ * Combine with .aggregate() to get grouped aggregate results.
537
+ */
538
+ groupBy(...cols: (keyof T & string)[]): SelectBuilder<T, S, TRelations>;
539
+ /**
540
+ * Add a HAVING clause — filters aggregate groups.
541
+ * Uses the same WhereInput system as .where() (supports operators, OR/AND).
542
+ *
543
+ * @example
544
+ * .groupBy('role').aggregate({ cnt: { fn: 'COUNT' } }).having({ cnt: { op: '>', value: 1 } })
545
+ */
546
+ having(conditions: WhereInput<Record<string, unknown>>): SelectBuilder<T, S, TRelations>;
547
+ /**
548
+ * Execute a GROUP BY + aggregate query.
549
+ * Returns typed rows with group-by columns + aggregate aliases.
550
+ *
551
+ * @example
552
+ * const rows = await db.from(orders)
553
+ * .groupBy('status')
554
+ * .aggregate({ total: { fn: 'SUM', col: 'amount' }, cnt: { fn: 'COUNT' } })
555
+ * // rows: { status: string; total: number; cnt: number }[]
556
+ */
557
+ aggregate<TAgg extends Record<string, number | string | null>>(aggregates: {
558
+ [K in keyof TAgg]: {
559
+ fn: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
560
+ col?: keyof T & string;
561
+ };
562
+ }): Promise<(Partial<T> & TAgg)[]>;
563
+ /** COUNT(*) or COUNT("col") — returns the count as a number. */
564
+ count(col?: keyof T & string): Promise<number>;
565
+ /** SUM("col") — returns the sum as a number (0 if no rows). */
566
+ sum(col: keyof T & string): Promise<number>;
567
+ /** AVG("col") — returns the average as a number (0 if no rows). */
568
+ avg(col: keyof T & string): Promise<number>;
569
+ /** MIN("col") — returns the minimum value. */
570
+ min(col: keyof T & string): Promise<number | string | null>;
571
+ /** MAX("col") — returns the maximum value. */
572
+ max(col: keyof T & string): Promise<number | string | null>;
573
+ private _scalarAggregate;
574
+ private _scalarAggregateRaw;
575
+ select(): Promise<T[]>;
576
+ first(): Promise<T | null>;
577
+ private _executeWith;
578
+ private _attachBelongsTo;
579
+ private _attachHasMany;
580
+ update(patch: Partial<T>): Promise<T>;
581
+ /**
582
+ * Update multiple rows atomically inside a single transaction.
583
+ * Each row must include the primary key. beforeUpdate and afterUpdate hooks
584
+ * run per row. If any row fails, the entire transaction rolls back.
585
+ *
586
+ * @example
587
+ * const updated = await db.from(usersTable).updateMany([
588
+ * { id: 1, name: 'Alice Updated' },
589
+ * { id: 2, role: 'admin' },
590
+ * ])
591
+ */
592
+ updateMany(rows: InferUpdate<S>[]): Promise<T[]>;
593
+ delete(): Promise<T>;
594
+ /**
595
+ * Soft-delete rows by setting the soft-delete column to the current timestamp.
596
+ * The table must have `.withSoftDelete()` configured — throws otherwise (at execute() time).
597
+ *
598
+ * Does NOT call beforeUpdate/afterUpdate hooks.
599
+ * Without .where(), all rows in the table are soft-deleted.
600
+ *
601
+ * @example
602
+ * await db.from(usersTable).softDelete().where({ id: 1 }).execute()
603
+ */
604
+ softDelete(): SoftDeleteBuilder<T, S>;
605
+ /**
606
+ * Restore soft-deleted rows by setting the soft-delete column back to null.
607
+ * The table must have `.withSoftDelete()` configured — throws otherwise (at execute() time).
608
+ *
609
+ * @example
610
+ * await db.from(usersTable).restore().where({ id: 1 }).execute()
611
+ */
612
+ restore(): SoftDeleteBuilder<T, S>;
613
+ }
614
+ declare class ColumnRestrictedBuilder<Col extends string, TCol, S extends SchemaMap, TRelations extends RelationsMap> {
615
+ private readonly _builder;
616
+ private readonly _col;
617
+ constructor(_builder: SelectBuilder<unknown, S, TRelations>, _col: Col);
618
+ where(conditions: WhereInput<Record<string, unknown>>): ColumnRestrictedBuilder<Col, TCol, S, TRelations>;
619
+ limit(n: number): ColumnRestrictedBuilder<Col, TCol, S, TRelations>;
620
+ orderBy(col: Col, dir?: 'ASC' | 'DESC'): ColumnRestrictedBuilder<Col, TCol, S, TRelations>;
621
+ /**
622
+ * Build the SQL for this query as a subquery fragment.
623
+ * The result can be used directly in WHERE IN / NOT IN conditions.
624
+ *
625
+ * @example
626
+ * const activeIds = db.from(usersTable).columns('id').where({ active: true }).subquery()
627
+ * // → SubqueryResult<'id', number>
628
+ *
629
+ * const posts = await db.from(postsTable)
630
+ * .where({ authorId: { op: 'IN', value: activeIds } })
631
+ * .select()
632
+ */
633
+ subquery(): SubqueryResult<Col, TCol>;
634
+ /** Build raw SELECT SQL + params without parentheses (for UNION). */
635
+ _buildRawSQL(): {
636
+ sql: string;
637
+ params: BindingValue[];
638
+ };
639
+ /**
640
+ * Combine this query with another same-type column query via UNION (deduplicates).
641
+ * Both sides must produce the same column type — enforced at compile time.
642
+ *
643
+ * @example
644
+ * db.from(usersTable).columns('id')
645
+ * .union(db.from(adminsTable).columns('id'))
646
+ * .select()
647
+ */
648
+ union(other: ColumnRestrictedBuilder<string, TCol, SchemaMap, RelationsMap>): UnionBuilder<TCol>;
649
+ /**
650
+ * Combine via UNION ALL — keeps duplicate rows.
651
+ */
652
+ unionAll(other: ColumnRestrictedBuilder<string, TCol, SchemaMap, RelationsMap>): UnionBuilder<TCol>;
653
+ }
654
+ declare class SoftDeleteBuilder<T, S extends SchemaMap> {
655
+ private readonly adapter;
656
+ private readonly table;
657
+ private readonly _value;
658
+ private readonly _dialect;
659
+ private _conditions;
660
+ constructor(adapter: VelnAdapter, table: TableDef<T, S>, _value: Date | null, _dialect?: SqlDialect);
661
+ /**
662
+ * Add WHERE conditions to scope which rows are soft-deleted / restored.
663
+ * Multiple calls accumulate with AND.
664
+ * Without .where(), all rows in the table are affected.
665
+ */
666
+ where(conditions: WhereInput<T>): this;
667
+ /**
668
+ * Execute the soft-delete or restore UPDATE.
669
+ * Throws if the table has no softDeleteColumn configured.
670
+ */
671
+ execute(): Promise<void>;
672
+ }
673
+ declare class UnionBuilder<T> {
674
+ private readonly _parts;
675
+ private readonly _kind;
676
+ private readonly _adapter;
677
+ private readonly _dialect;
678
+ private _orderBy?;
679
+ private _limit?;
680
+ constructor(_parts: Array<{
681
+ sql: string;
682
+ params: BindingValue[];
683
+ }>, _kind: 'UNION' | 'UNION ALL', _adapter: VelnAdapter, _dialect: SqlDialect);
684
+ /** Append another UNION (deduplicating) leg. */
685
+ union(other: ColumnRestrictedBuilder<string, T, SchemaMap, RelationsMap>): UnionBuilder<T>;
686
+ /** Append another UNION ALL (keep duplicates) leg. */
687
+ unionAll(other: ColumnRestrictedBuilder<string, T, SchemaMap, RelationsMap>): UnionBuilder<T>;
688
+ /** Add ORDER BY to the entire UNION result. */
689
+ orderBy(col: string, dir?: 'ASC' | 'DESC'): UnionBuilder<T>;
690
+ /** Add LIMIT to the entire UNION result. */
691
+ limit(n: number): UnionBuilder<T>;
692
+ /** Execute the UNION query and return typed rows. */
693
+ select(): Promise<Record<string, T>[]>;
694
+ /**
695
+ * Build the UNION as a subquery — wrapped in parentheses.
696
+ * Usable in WHERE IN / NOT IN conditions.
697
+ *
698
+ * @example
699
+ * const adminOrModIds = db.from(usersTable).columns('id').where({ role: 'admin' })
700
+ * .union(db.from(usersTable).columns('id').where({ role: 'mod' }))
701
+ * .subquery()
702
+ */
703
+ subquery(): SubqueryResult<string, T>;
704
+ }
705
+ declare class InsertBuilder<T, S extends SchemaMap> {
706
+ private readonly adapter;
707
+ private readonly hooks;
708
+ private readonly ctx;
709
+ private readonly queue;
710
+ private readonly table;
711
+ private readonly dialect;
712
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S>, dialect?: SqlDialect);
713
+ insert(data: InferInsert<S>): Promise<T>;
714
+ /**
715
+ * Insert multiple rows in a single SQL statement.
716
+ * beforeInsert and afterInsert hooks run per row.
717
+ * Defaults (defaultFn / defaultValue) are applied per row.
718
+ *
719
+ * MySQL is not yet supported (no RETURNING *) — throws an informative error.
720
+ *
721
+ * @example
722
+ * const users = await db.into(usersTable).insertMany([
723
+ * { name: 'Alice', email: 'alice@example.com' },
724
+ * { name: 'Bob', email: 'bob@example.com' },
725
+ * ])
726
+ */
727
+ insertMany(data: InferInsert<S>[]): Promise<T[]>;
728
+ /** Serialize values for storage. Date → ISO string. Drops undefined values. */
729
+ private _serializeForInsert;
730
+ }
731
+ declare class JoinBuilder {
732
+ private readonly adapter;
733
+ private readonly tableName;
734
+ private readonly _columns;
735
+ private readonly _joins;
736
+ private readonly _where;
737
+ private readonly _params;
738
+ private readonly _options;
739
+ constructor(adapter: VelnAdapter, tableName: string, _columns: string[], _joins: JoinClause[], _where: string, _params: BindingValue[], _options?: SelectOptions);
740
+ private _cloneOpts;
741
+ /** Restrict the selected columns (e.g. ['orders.id', 'users.name']). */
742
+ columns(cols: string[]): JoinBuilder;
743
+ /** Add an INNER JOIN clause. */
744
+ join(table: string, on: string): JoinBuilder;
745
+ /** Add a LEFT JOIN clause. */
746
+ leftJoin(table: string, on: string): JoinBuilder;
747
+ /** Add a RIGHT JOIN clause. */
748
+ rightJoin(table: string, on: string): JoinBuilder;
749
+ /** Add a FULL JOIN clause. */
750
+ fullJoin(table: string, on: string): JoinBuilder;
751
+ /**
752
+ * Set a typed WHERE clause from a conditions object.
753
+ * Each key becomes "key" = ? — values are bound safely.
754
+ *
755
+ * @example
756
+ * .where({ status: 'pending', userId })
757
+ */
758
+ where(conditions: Record<string, BindingValue>): JoinBuilder;
759
+ /**
760
+ * Set a raw WHERE clause.
761
+ * Use ? as placeholder; pass bind values as the second argument.
762
+ *
763
+ * @example
764
+ * .where('orders.status = ? AND orders.user_id = ?', ['pending', userId])
765
+ */
766
+ where(sql: string, params: BindingValue[]): JoinBuilder;
767
+ /** Limit the number of rows returned. Bound as a parameter — never interpolated. */
768
+ limit(n: number): JoinBuilder;
769
+ /** Skip the first n rows. Bound as a parameter — never interpolated. */
770
+ offset(n: number): JoinBuilder;
771
+ /**
772
+ * Add an ORDER BY clause. Multiple calls accumulate in order.
773
+ * @param col Column reference, e.g. 'orders.created_at' or 'name'.
774
+ */
775
+ orderBy(col: string, dir?: 'ASC' | 'DESC'): JoinBuilder;
776
+ /**
777
+ * Convenience helper for cursor-based pagination.
778
+ * page(1, 10) → LIMIT 10 OFFSET 0
779
+ * page(2, 10) → LIMIT 10 OFFSET 10
780
+ */
781
+ page(page: number, size: number): JoinBuilder;
782
+ /**
783
+ * Execute the query and return raw rows (no deserialization).
784
+ *
785
+ * @remarks
786
+ * Type parameter T is a manual cast — not validated at runtime.
787
+ * For runtime validation, use db.raw() with a Zod schema instead.
788
+ */
789
+ select<T = Record<string, unknown>>(): Promise<T[]>;
790
+ /**
791
+ * Execute the query and return the first row, or null.
792
+ *
793
+ * @remarks
794
+ * Type parameter T is a manual cast — not validated at runtime.
795
+ */
796
+ first<T = Record<string, unknown>>(): Promise<T | null>;
797
+ private _addJoin;
798
+ }
799
+
800
+ interface WsCtx<TData = unknown> {
801
+ /** Matched path params, e.g. { roomId: '42' } for /rooms/:roomId */
802
+ params: Record<string, string>;
803
+ /** Parsed query string from the upgrade request */
804
+ query: Record<string, string | string[]>;
805
+ /**
806
+ * The Bun ServerWebSocket — use ws.send(), ws.close(), ws.subscribe(), etc.
807
+ * Typed with `data: WsCtxData` so ws.data carries ctx.user etc.
808
+ */
809
+ ws: bun.ServerWebSocket<WsCtxData>;
810
+ /**
811
+ * Validated & typed message payload.
812
+ * Only set inside onMessage() when a message schema was defined.
813
+ * Undefined in open / close / drain.
814
+ */
815
+ data: TData;
816
+ /** Set by jwtPlugin when registered on the app. undefined otherwise. */
817
+ user?: AuthPayload;
818
+ /** Set by dbPlugin when registered on the app. undefined otherwise. */
819
+ db?: BoundVelnDB;
820
+ }
821
+ interface WsCtxData {
822
+ _wsPath: string;
823
+ params: Record<string, string>;
824
+ query: Record<string, string | string[]>;
825
+ user?: AuthPayload;
826
+ db?: BoundVelnDB;
827
+ [key: string]: unknown;
828
+ _data?: unknown;
829
+ }
830
+ interface WsHandlers<TMsg = unknown> {
831
+ /** Called when a client opens a connection. ctx.data is undefined here. */
832
+ open?: (ctx: WsCtx<undefined>) => void | Promise<void>;
833
+ /**
834
+ * Called for each incoming message.
835
+ * If a message schema was provided, ctx.data is the validated & typed payload.
836
+ */
837
+ message?: (ctx: WsCtx<TMsg>, raw: string | Uint8Array) => void | Promise<void>;
838
+ /**
839
+ * Called when a client disconnects.
840
+ * code: WebSocket close code; reason: human-readable string.
841
+ */
842
+ close?: (ctx: WsCtx<undefined>, code: number, reason: string) => void | Promise<void>;
843
+ /** Called when the send buffer is drained and more data can be written. */
844
+ drain?: (ctx: WsCtx<undefined>) => void | Promise<void>;
845
+ }
846
+ /** Stored internally — one entry per registered WS path. */
847
+ interface WsRoute {
848
+ /** Registered path pattern, e.g. '/chat' or '/rooms/:id' */
849
+ path: string;
850
+ /** Optional Zod schema to parse incoming messages */
851
+ messageSchema?: ZodTypeAny;
852
+ handlers: WsHandlers<any>;
853
+ /** Reference to the originating module (null for app-level) */
854
+ _module: unknown | null;
855
+ /** Index signature — satisfies WsRouteShape from core */
856
+ [key: string]: unknown;
857
+ }
858
+ /**
859
+ * WsRouteHandler — passed to app.ws() / module.ws().
860
+ *
861
+ * Usage (no schema):
862
+ * app.ws('/chat', { open(ctx) { ... }, message(ctx, raw) { ... } })
863
+ *
864
+ * Usage (with Zod schema — message is typed):
865
+ * app.ws('/chat', {
866
+ * message: z.object({ text: z.string() }),
867
+ * handlers: {
868
+ * message(ctx, raw) { ctx.data.text // ← typed }
869
+ * }
870
+ * })
871
+ */
872
+ type WsRouteHandler<TMsg = unknown> = WsHandlers<TMsg> | WsRouteHandlerWithSchema<TMsg>;
873
+ interface WsRouteHandlerWithSchema<TMsg = unknown> {
874
+ /** Zod schema for incoming messages. Parsed before onMessage is called. */
875
+ message: ZodTypeAny;
876
+ /** Lifecycle callbacks — message(ctx) receives the validated & typed payload */
877
+ handlers: WsHandlers<TMsg>;
878
+ }
879
+
880
+ declare module 'oakbun' {
881
+ interface ModuleBuilder<TCtx, TPrefix extends string, TRoutes> {
882
+ ws<TMsg = unknown>(path: string, handler: WsRouteHandler<TMsg>): ModuleBuilder<TCtx, TPrefix, TRoutes>;
883
+ }
884
+ }
885
+
886
+ declare const _baseAuditFields: {
887
+ readonly id: oakbun.Column<number>;
888
+ readonly tableName: oakbun.Column<string>;
889
+ readonly operation: oakbun.Column<string>;
890
+ readonly actor: oakbun.Column<string | null>;
891
+ readonly before: oakbun.Column<string | null>;
892
+ readonly after: oakbun.Column<string | null>;
893
+ readonly changedAt: oakbun.Column<Date>;
894
+ };
895
+ type BaseAuditSchema = typeof _baseAuditFields;
896
+ type AuditTableDef<S extends SchemaMap = BaseAuditSchema> = TableDef<InferRow<BaseAuditSchema & S>, BaseAuditSchema & S>;
897
+ interface AuditConfig<TCtx, TRow, S extends SchemaMap = BaseAuditSchema> {
898
+ /** The audit table to write into. */
899
+ storeIn: AuditTableDef<S>;
900
+ /** Extract actor identifier from request context. Return null for anonymous. */
901
+ actor: (ctx: TCtx) => string | null | undefined;
902
+ /** Field names to replace with '[REDACTED]' in before/after snapshots. */
903
+ redact?: (keyof TRow & string)[];
904
+ /** Called when an audit write fails. Defaults to console.error. */
905
+ onError?: (err: unknown) => void;
906
+ }
907
+
908
+ type ModelInstance<TDef> = TDef & {
909
+ readonly db: BoundVelnDB;
910
+ };
911
+ interface ModelDef<TName extends string, TDef> {
912
+ readonly _modelName: TName;
913
+ readonly _factory: (db: BoundVelnDB) => ModelInstance<TDef>;
914
+ }
915
+
916
+ type Dep<TKey extends string, TDef> = ModelDef<TKey, TDef> | ServiceDef<TKey, TDef>;
917
+ interface ServiceDef<TKey extends string, TDef> {
918
+ readonly _serviceKey: TKey;
919
+ readonly _deps: ReadonlyArray<Dep<string, unknown>>;
920
+ readonly _options: BaseOptions;
921
+ readonly _factory: (deps: Record<string, unknown>) => TDef;
922
+ }
923
+
924
+ type EventCallback = (payload: unknown) => void | Promise<void>;
925
+ type RawHandler = (payload: unknown, ctx: Record<string, unknown>) => void | Promise<void>;
926
+ interface EventHandlerDef {
927
+ readonly _handlers: Map<string, EventCallback>;
928
+ readonly _rawHandlers: Map<string, RawHandler>;
929
+ readonly _logger: Logger;
930
+ readonly _services: ReadonlyArray<ServiceDef<string, unknown>>;
931
+ }
932
+
933
+ interface CronCtx {
934
+ db: BoundVelnDB;
935
+ [key: string]: unknown;
936
+ }
937
+
938
+ interface CronDef<TServices extends Record<string, unknown> = Record<never, never>> {
939
+ readonly _name: string;
940
+ readonly _expression: string;
941
+ readonly _timezone: string | undefined;
942
+ readonly _runOnStart: boolean;
943
+ readonly _ttlMs: number | undefined;
944
+ readonly _logger: Logger;
945
+ readonly _mode: 'process' | 'os';
946
+ readonly _handler: ((ctx: CronCtx & TServices, logger: Logger) => Promise<void> | void) | undefined;
947
+ readonly _services: ReadonlyArray<ServiceDef<string, unknown>>;
948
+ readonly _script: string | undefined;
949
+ readonly _onError: ((err: unknown) => void) | undefined;
950
+ use<TKey extends string, TDef>(service: ServiceDef<TKey, TDef>): CronDef<TServices & Record<TKey, TDef>>;
951
+ }
952
+
953
+ interface HookDeclaration<T, TCtx> {
954
+ table: TableDef<T, any>;
955
+ handlers: ModuleHookHandlers<T, TCtx>;
956
+ }
957
+ interface AuditDeclaration<T, TCtx, S extends SchemaMap> {
958
+ table: TableDef<T, any>;
959
+ config: AuditConfig<TCtx, T, S>;
960
+ }
961
+ interface ServiceDeclaration<TKey extends string, TDef> {
962
+ readonly service: ServiceDef<TKey, TDef>;
963
+ }
964
+ interface VelnModule {
965
+ prefix: string;
966
+ routes: Route<any>[];
967
+ wsRoutes: WsRouteShape[];
968
+ hookDeclarations: HookDeclaration<any, any>[];
969
+ auditDeclarations: AuditDeclaration<any, any, any>[];
970
+ serviceDeclarations: ReadonlyArray<ServiceDeclaration<string, unknown>>;
971
+ plugins: Plugin<any, any>[];
972
+ guards: Guard<any>[];
973
+ onRequestHooks: OnRequestHook<any>[];
974
+ onBeforeHandleHooks: OnBeforeHandleHook<any>[];
975
+ onResponseHooks: OnResponseHook<any>[];
976
+ onError?: ErrorHandler<any>;
977
+ eventHandlerDefs: EventHandlerDef[];
978
+ cronDefs: CronDef<Record<string, unknown>>[];
979
+ visibility: 'public' | 'hidden';
980
+ meta?: {
981
+ tag?: string;
982
+ description?: string;
983
+ };
984
+ options?: BaseOptions;
985
+ }
986
+
987
+ interface CookieOptions {
988
+ httpOnly?: boolean;
989
+ secure?: boolean;
990
+ sameSite?: 'Strict' | 'Lax' | 'None';
991
+ maxAge?: number;
992
+ path?: string;
993
+ domain?: string;
994
+ }
995
+ interface CookieJar {
996
+ get(name: string): string | undefined;
997
+ set(name: string, value: string, options?: CookieOptions): void;
998
+ delete(name: string): void;
999
+ /** Framework-internal: returns all pending Set-Cookie header values */
1000
+ _pending(): string[];
1001
+ }
1002
+
1003
+ /** Minimal WS route shape that Core knows about. Full type lives in @veln/ws. */
1004
+ interface WsRouteShape {
1005
+ path: string;
1006
+ _module: unknown | null;
1007
+ [key: string]: unknown;
1008
+ }
1009
+ interface RouteSchema {
1010
+ params?: ZodTypeAny;
1011
+ query?: ZodTypeAny;
1012
+ body?: ZodTypeAny;
1013
+ response?: ZodTypeAny;
1014
+ }
1015
+ /** Additional response code definition for OpenAPI docs (e.g. 401, 404). */
1016
+ interface RouteResponseDoc {
1017
+ description: string;
1018
+ }
1019
+ interface RouteDocs {
1020
+ /** Human-readable route summary shown in Scalar / Swagger UI. Auto-generated if absent. */
1021
+ summary?: string;
1022
+ /** Longer description rendered below the summary. Markdown supported. */
1023
+ description?: string;
1024
+ /** Unique operationId. Auto-generated if absent (e.g. "listUsers", "getUserById"). */
1025
+ operationId?: string;
1026
+ /**
1027
+ * Additional HTTP response codes to document in the OpenAPI spec.
1028
+ * The 200 success response is always generated automatically.
1029
+ * Use this to document error responses like 401, 403, 404, 422, etc.
1030
+ *
1031
+ * @example
1032
+ * docs: {
1033
+ * responses: {
1034
+ * 401: { description: 'Unauthorized' },
1035
+ * 404: { description: 'Not found' },
1036
+ * }
1037
+ * }
1038
+ */
1039
+ responses?: Record<number, RouteResponseDoc>;
1040
+ }
1041
+ /** Controls a streaming response. Passed to the ctx.stream() writer callback. */
1042
+ interface StreamController {
1043
+ /** Push a string or binary chunk to the stream. */
1044
+ send(chunk: string | Uint8Array): void;
1045
+ /** Close the stream. Must be called to end the response. */
1046
+ close(): void;
1047
+ }
1048
+ /** Options for ctx.stream(). */
1049
+ interface StreamOptions {
1050
+ /**
1051
+ * Content-Type header for the streaming response.
1052
+ * Defaults to `'text/plain'`.
1053
+ * Use `'text/event-stream'` for SSE, `'application/x-ndjson'` for NDJSON.
1054
+ */
1055
+ contentType?: string;
1056
+ /** Additional headers to include in the response. */
1057
+ headers?: Record<string, string>;
1058
+ /** HTTP status code. Defaults to 200. */
1059
+ status?: number;
1060
+ }
1061
+ /**
1062
+ * Controls a Server-Sent Events stream.
1063
+ * Passed to the ctx.sse() writer callback.
1064
+ *
1065
+ * SSE wire format:
1066
+ * event: <name>\ndata: <json>\n\n — named event
1067
+ * data: <json>\n\n — unnamed event
1068
+ * : <text>\n\n — comment / keepalive
1069
+ * id: <value>\n — event ID for reconnect
1070
+ * retry: <ms>\n — reconnect interval
1071
+ */
1072
+ interface SseController {
1073
+ /** Send a named event with a JSON-serializable payload. */
1074
+ event(name: string, data: unknown): Promise<void>;
1075
+ /** Send an unnamed data event. */
1076
+ data(data: unknown): Promise<void>;
1077
+ /** Send an SSE comment (e.g. keepalive ping). */
1078
+ comment(text?: string): Promise<void>;
1079
+ /** Set the last event ID (used by the browser for reconnect). */
1080
+ id(id: string): Promise<void>;
1081
+ /** Tell the browser how long to wait before reconnecting (milliseconds). */
1082
+ retry(ms: number): Promise<void>;
1083
+ }
1084
+ interface Logger {
1085
+ info(msg: string, ...args: unknown[]): void;
1086
+ warn(msg: string, ...args: unknown[]): void;
1087
+ error(msg: string, ...args: unknown[]): void;
1088
+ debug(msg: string, ...args: unknown[]): void;
1089
+ }
1090
+ interface LogOptions {
1091
+ /** Minimum level to emit. Defaults to 'info'. */
1092
+ level?: 'debug' | 'info' | 'warn' | 'error';
1093
+ /** Keys whose values are replaced with '***' in structured data. Case-insensitive. */
1094
+ mask?: string[];
1095
+ /** Suppress all output — useful in tests. */
1096
+ silent?: boolean;
1097
+ }
1098
+ interface BaseOptions {
1099
+ log?: LogOptions;
1100
+ }
1101
+ interface BaseCtx {
1102
+ req: Request;
1103
+ params: Record<string, string>;
1104
+ query: Record<string, string | string[]>;
1105
+ body?: unknown;
1106
+ json: <T>(data: T, status?: number) => Response;
1107
+ text: (data: string, status?: number) => Response;
1108
+ html: (data: string, status?: number) => Response;
1109
+ /**
1110
+ * Returns a streaming Response backed by a ReadableStream.
1111
+ *
1112
+ * The callback receives a StreamController — call `send(chunk)` to push data
1113
+ * and `close()` to end the stream. Errors thrown inside are caught automatically.
1114
+ *
1115
+ * Set `contentType` for SSE (`'text/event-stream'`) or NDJSON (`'application/x-ndjson'`).
1116
+ * Compression is automatically skipped for streaming responses.
1117
+ *
1118
+ * @example
1119
+ * return ctx.stream((stream) => {
1120
+ * stream.send('data: hello\n\n')
1121
+ * stream.send('data: world\n\n')
1122
+ * stream.close()
1123
+ * }, { contentType: 'text/event-stream' })
1124
+ */
1125
+ stream: (writer: (controller: StreamController) => void | Promise<void>, options?: StreamOptions) => Response;
1126
+ /**
1127
+ * Returns a Server-Sent Events Response.
1128
+ *
1129
+ * The callback receives an SseController — call `event()`, `data()`,
1130
+ * `comment()`, `id()`, or `retry()` to push SSE frames, then let the
1131
+ * callback return (or the async iterator complete) to close the stream.
1132
+ *
1133
+ * @example
1134
+ * return ctx.sse(async (sse) => {
1135
+ * await sse.event('connected', { userId: '42' })
1136
+ * for await (const update of source()) {
1137
+ * await sse.event('update', update)
1138
+ * await sse.comment('keepalive')
1139
+ * }
1140
+ * })
1141
+ */
1142
+ sse: (writer: (controller: SseController) => void | Promise<void>) => Response;
1143
+ events?: EventBus;
1144
+ logger?: Logger;
1145
+ db?: BoundVelnDB;
1146
+ cookie: CookieJar;
1147
+ emit: <K extends keyof VelnEvents>(event: K, payload: VelnEvents[K]) => void;
1148
+ _requestQueue?: RequestEventQueue;
1149
+ _queryLog?: QueryLog;
1150
+ _startTime?: number;
1151
+ }
1152
+ type Guard<TCtx> = (ctx: TCtx) => Response | null | Promise<Response | null>;
1153
+ type ErrorHandler<TCtx = BaseCtx> = (err: unknown, ctx: TCtx) => Response | Promise<Response>;
1154
+ interface RouteHandler<TCtx> {
1155
+ handler: (ctx: TCtx) => Response | Promise<Response>;
1156
+ }
1157
+ type OnRequestFn<TCtx extends BaseCtx = BaseCtx> = (ctx: TCtx) => Response | void | Promise<Response | void>;
1158
+ type OnBeforeHandleFn<TCtx extends BaseCtx = BaseCtx> = (ctx: TCtx) => Response | void | Promise<Response | void>;
1159
+ type OnResponseFn<TCtx extends BaseCtx = BaseCtx> = (ctx: TCtx, response: Response) => Response | void | Promise<Response | void>;
1160
+ interface OnRequestHook<TCtx extends BaseCtx = BaseCtx> {
1161
+ readonly _phase: 'onRequest';
1162
+ readonly _fn: OnRequestFn<TCtx>;
1163
+ }
1164
+ interface OnBeforeHandleHook<TCtx extends BaseCtx = BaseCtx> {
1165
+ readonly _phase: 'onBeforeHandle';
1166
+ readonly _fn: OnBeforeHandleFn<TCtx>;
1167
+ }
1168
+ interface OnResponseHook<TCtx extends BaseCtx = BaseCtx> {
1169
+ readonly _phase: 'onResponse';
1170
+ readonly _fn: OnResponseFn<TCtx>;
1171
+ }
1172
+ interface Route<TCtx = BaseCtx> {
1173
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
1174
+ path: string;
1175
+ summary?: string;
1176
+ description?: string;
1177
+ /** OpenAPI documentation override set via the `docs` option on route registration. */
1178
+ docs?: RouteDocs;
1179
+ handler: RouteHandler<TCtx>;
1180
+ guards: Guard<TCtx>[];
1181
+ onError?: ErrorHandler<TCtx>;
1182
+ schema?: RouteSchema;
1183
+ visibility?: 'public' | 'hidden';
1184
+ moduleGuardOptOut?: true;
1185
+ _module?: VelnModule;
1186
+ _pluginName?: string;
1187
+ }
1188
+
1189
+ /** A single navigation item contributed by a plugin via .nav(). */
1190
+ interface NavItem {
1191
+ label: string;
1192
+ route: string;
1193
+ icon?: string;
1194
+ order?: number;
1195
+ children?: NavItem[];
1196
+ }
1197
+ interface Plugin<TCtx, TAdd extends object> {
1198
+ name: string;
1199
+ /**
1200
+ * Optional list of plugin names that must be registered before this plugin.
1201
+ * app.plugin() validates this at registration time and throws PLUGIN_MISSING_DEP
1202
+ * if a required plugin is not yet registered.
1203
+ *
1204
+ * Example: eventBusPlugin sets requires: ['db'] to enforce registration order.
1205
+ */
1206
+ requires?: string[];
1207
+ /**
1208
+ * Optional list of modules this plugin contributes.
1209
+ * app.plugin() calls app.register() on each entry automatically.
1210
+ *
1211
+ * Can also be set to a factory function for typed ctx inference (Option A, Spec 04).
1212
+ * The factory is called once with a dummy ctx to extract the module list —
1213
+ * it is NEVER called at request time.
1214
+ */
1215
+ modules?: VelnModule[];
1216
+ /**
1217
+ * Optional permission gate for all routes contributed via .modules().
1218
+ * app.plugin() checks ctx.user before running plugin.request() for those routes.
1219
+ * User must have at least one of the listed permissions — checked via AuthAdapter.hasPermission().
1220
+ * No user → 401. User without any matching permission → 403.
1221
+ */
1222
+ permissions?: string[];
1223
+ /**
1224
+ * Optional nav items contributed by this plugin.
1225
+ * GET /nav returns these filtered by the plugin's permissions for the current user.
1226
+ */
1227
+ nav?: NavItem[];
1228
+ install?: (hooks: HookExecutor) => Promise<void> | void;
1229
+ request: (ctx: TCtx) => Promise<TCtx & TAdd> | (TCtx & TAdd);
1230
+ teardown?: () => Promise<void> | void;
1231
+ }
1232
+
1233
+ interface WsRateLimitConfig {
1234
+ /** Maximum messages per window per connection. Default: 60 */
1235
+ max?: number;
1236
+ /** Window duration in milliseconds. Default: 1000 (1 second) */
1237
+ windowMs?: number;
1238
+ }
1239
+ declare class VelnWsAdapterImpl implements VelnWsAdapter {
1240
+ private readonly _routes;
1241
+ private readonly _rateLimitMap;
1242
+ private readonly _rateLimitMax;
1243
+ private readonly _rateLimitWindowMs;
1244
+ constructor(rateLimit?: WsRateLimitConfig);
1245
+ registerRoute(path: string, route: WsRouteShape$1): void;
1246
+ getRoute(path: string): WsRoute | undefined;
1247
+ /**
1248
+ * ws() — register a typed WebSocket route on this adapter.
1249
+ *
1250
+ * Usage:
1251
+ * const ws = createWsAdapter()
1252
+ * app.registerWsAdapter(ws)
1253
+ *
1254
+ * ws.route('/chat', {
1255
+ * open(ctx) { ctx.ws.send('welcome') },
1256
+ * message(ctx, raw) { ctx.ws.send(raw) },
1257
+ * })
1258
+ *
1259
+ * // With Zod message schema:
1260
+ * ws.route('/chat', {
1261
+ * message: z.object({ text: z.string() }),
1262
+ * handlers: { message(ctx) { ctx.data.text } },
1263
+ * })
1264
+ */
1265
+ route<TMsg = unknown>(path: string, handler: WsRouteHandler<TMsg>): this;
1266
+ handleUpgrade(req: Request, server: bun.Server, baseCtx: BaseCtx$1, plugins: ReadonlyArray<Plugin<any, any>>, installedRef: {
1267
+ value: boolean;
1268
+ }, installedModulePlugins: Set<string>): Promise<Response | null>;
1269
+ getWebsocketConfig(): bun.WebSocketHandler<Record<string, unknown>>;
1270
+ private _buildCtx;
1271
+ }
1272
+ /**
1273
+ * createWsAdapter() — creates a VelnWsAdapter for use with Veln apps.
1274
+ *
1275
+ * Usage:
1276
+ * import { createWsAdapter } from '@oakbun/ws'
1277
+ * app.registerWsAdapter(createWsAdapter())
1278
+ *
1279
+ * app.ws('/chat', {
1280
+ * open(ctx) { ctx.ws.send('welcome') },
1281
+ * message(ctx, raw) { ctx.ws.send(raw) },
1282
+ * })
1283
+ */
1284
+ declare function createWsAdapter(rateLimit?: WsRateLimitConfig): VelnWsAdapterImpl;
1285
+
1286
+ export { VelnWsAdapterImpl, type WsCtx, type WsCtxData, type WsHandlers, type WsRateLimitConfig, type WsRoute, type WsRouteHandler, type WsRouteHandlerWithSchema, createWsAdapter };