@oakbun/ws 0.1.1 → 2.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 (2) hide show
  1. package/dist/index.d.ts +315 -27
  2. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -64,6 +64,11 @@ type InferInsertFromSchema<S extends SchemaMap> = {
64
64
  [K in keyof S as IsOptionalOnInsert<S[K]> extends true ? K : never]?: S[K] extends Column<infer T> ? T : never;
65
65
  };
66
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;
67
72
  interface TableHookHandlers<T> {
68
73
  beforeInsert?: (data: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
69
74
  afterInsert?: (result: T, input: Partial<T>) => void | Promise<void>;
@@ -87,13 +92,66 @@ type InferTableEvents<T, M extends TableEventMap> = (M['afterInsert'] extends st
87
92
  } : Record<never, never>) & (M['afterDelete'] extends string ? {
88
93
  [_ in M['afterDelete']]: T;
89
94
  } : Record<never, never>);
90
- interface TableDef<T, S extends SchemaMap = SchemaMap, TEvents extends TableEventMap = TableEventMap> {
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> {
91
145
  readonly name: string;
92
146
  readonly schema: S;
93
147
  readonly primaryKey: keyof T & string;
94
148
  readonly hooks: TableHookHandlers<T>[];
95
149
  readonly events: TEvents;
96
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;
97
155
  }
98
156
 
99
157
  interface ModuleHookHandlers<T, TCtx = unknown> {
@@ -168,6 +226,21 @@ declare class HookExecutor {
168
226
  private _moduleHandlers;
169
227
  }
170
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
+ }
171
244
  interface JoinClause {
172
245
  type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
173
246
  table: string;
@@ -175,6 +248,8 @@ interface JoinClause {
175
248
  }
176
249
  /** SQL dialect — used for ILIKE fallback on non-Postgres adapters. */
177
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>;
178
253
  /** Explicit operator condition for a single column. */
179
254
  type WhereOp<T> = {
180
255
  op: '=';
@@ -196,10 +271,10 @@ type WhereOp<T> = {
196
271
  value: T;
197
272
  } | {
198
273
  op: 'IN';
199
- value: T[];
274
+ value: InValue<T>;
200
275
  } | {
201
276
  op: 'NOT IN';
202
- value: T[];
277
+ value: InValue<T>;
203
278
  } | {
204
279
  op: 'LIKE';
205
280
  value: string;
@@ -243,6 +318,7 @@ interface SelectOptions {
243
318
  groupBy?: string[];
244
319
  aggregates?: AggregateClause[];
245
320
  having?: WhereInput<Record<string, unknown>>;
321
+ distinct?: boolean;
246
322
  }
247
323
 
248
324
  interface QueryLog {
@@ -269,11 +345,12 @@ declare class BoundVelnDB {
269
345
  private readonly hooks;
270
346
  private readonly ctx;
271
347
  private readonly queue?;
348
+ private readonly dialect;
272
349
  /** Per-request query counter — incremented for every query() and execute() call on this instance. */
273
350
  _queryCount: number;
274
351
  private readonly adapter;
275
- constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue?: RequestEventQueue | undefined, queryLog?: QueryLog);
276
- from<T, S extends SchemaMap>(table: TableDef<T, S>): SelectBuilder<T, S>;
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>;
277
354
  /**
278
355
  * Start a JOIN query from the given table name.
279
356
  * Returns a JoinBuilder — call .join()/.leftJoin() etc. to add clauses,
@@ -293,22 +370,36 @@ declare class BoundVelnDB {
293
370
  * Returns a Map keyed by the foreign-key value; each entry is an array of
294
371
  * matching child rows (for one-to-many relations).
295
372
  *
296
- * @example
297
- * const posts = await db.from(postsTable).select()
373
+ * Two call forms:
374
+ *
375
+ * @example — explicit (original)
298
376
  * const authorMap = await db.loadRelation(posts, 'authorId', usersTable, 'id')
299
- * // → SELECT * FROM "users" WHERE "id" IN (1, 2, 3)
300
- * const withAuthors = posts.map(p => ({ ...p, author: authorMap.get(p.authorId)?.[0] ?? null }))
377
+ *
378
+ * @example name-based (reads relation metadata declared on the table)
379
+ * const authorMap = await db.loadRelation(posts, 'author', postsTable)
301
380
  */
302
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>>;
303
383
  /**
304
384
  * Convenience variant of loadRelation for belongs-to (many-to-one) relations.
305
385
  * Returns Map<fkValue, TChild> — single child per key instead of an array.
306
386
  *
307
- * @example
387
+ * Two call forms:
388
+ *
389
+ * @example — explicit (original)
308
390
  * const authorMap = await db.loadRelationOne(posts, 'authorId', usersTable, 'id')
309
- * const author = authorMap.get(post.authorId) ?? null
391
+ *
392
+ * @example — name-based
393
+ * const authorMap = await db.loadRelationOne(posts, 'author', postsTable)
310
394
  */
311
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;
312
403
  transaction<T>(fn: (db: BoundVelnDB) => Promise<T>): Promise<TransactionResult<T>>;
313
404
  /**
314
405
  * Execute raw SQL and return typed rows.
@@ -329,7 +420,7 @@ declare class BoundVelnDB {
329
420
  parse: (row: unknown) => T;
330
421
  }): Promise<T[]>;
331
422
  }
332
- declare class SelectBuilder<T, S extends SchemaMap> {
423
+ declare class SelectBuilder<T, S extends SchemaMap, TRelations extends RelationsMap = RelationsMap> {
333
424
  private readonly adapter;
334
425
  private readonly hooks;
335
426
  private readonly ctx;
@@ -339,12 +430,39 @@ declare class SelectBuilder<T, S extends SchemaMap> {
339
430
  private readonly _options;
340
431
  private readonly _rawWhere;
341
432
  private readonly _dialect;
342
- constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S>, conditions: WhereInput<T>, _options?: SelectOptions, _rawWhere?: {
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?: {
343
436
  sql: string;
344
437
  params: BindingValue[];
345
- }[], _dialect?: SqlDialect);
438
+ }[], _dialect?: SqlDialect, _withRelations?: string[], _includeDeleted?: boolean);
346
439
  private _cloneWith;
347
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>;
348
466
  /**
349
467
  * Add WHERE conditions. Accepts:
350
468
  * - Plain equality map: `.where({ role: 'admin' })`
@@ -354,7 +472,7 @@ declare class SelectBuilder<T, S extends SchemaMap> {
354
472
  *
355
473
  * Multiple `.where()` calls are combined with AND.
356
474
  */
357
- where(conditions: WhereInput<T>): SelectBuilder<T, S>;
475
+ where(conditions: WhereInput<T>): SelectBuilder<T, S, TRelations>;
358
476
  /**
359
477
  * Append a raw SQL WHERE fragment, combined with AND.
360
478
  * Use for conditions the builder cannot express.
@@ -363,31 +481,61 @@ declare class SelectBuilder<T, S extends SchemaMap> {
363
481
  * .whereRaw('"score" > "threshold"', [])
364
482
  * .whereRaw('"created_at" > ?', ['2024-01-01'])
365
483
  */
366
- whereRaw(sql: string, params: BindingValue[]): SelectBuilder<T, S>;
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>;
367
494
  /** Limit the number of rows returned. Bound as a parameter — never interpolated. */
368
- limit(n: number): SelectBuilder<T, S>;
495
+ limit(n: number): SelectBuilder<T, S, TRelations>;
369
496
  /** Skip the first n rows. Bound as a parameter — never interpolated. */
370
- offset(n: number): SelectBuilder<T, S>;
497
+ offset(n: number): SelectBuilder<T, S, TRelations>;
371
498
  /** Add an ORDER BY clause. Multiple calls accumulate in order. */
372
- orderBy(col: keyof T & string, dir?: 'ASC' | 'DESC'): SelectBuilder<T, S>;
499
+ orderBy(col: keyof T & string, dir?: 'ASC' | 'DESC'): SelectBuilder<T, S, TRelations>;
373
500
  /**
374
501
  * Convenience helper for cursor-based pagination.
375
502
  * page(1, 10) → LIMIT 10 OFFSET 0
376
503
  * page(2, 10) → LIMIT 10 OFFSET 10
377
504
  */
378
- page(page: number, size: number): SelectBuilder<T, S>;
505
+ page(page: number, size: number): SelectBuilder<T, S, TRelations>;
379
506
  /**
380
507
  * Restrict which columns are returned.
381
- * SELECT "id", "name" FROM "table" — instead of SELECT *
382
508
  *
383
- * Return type is narrowed to Pick<T, K> for full type safety.
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().
384
520
  */
385
- columns<K extends keyof T & string>(...cols: K[]): SelectBuilder<Pick<T, K>, S>;
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
+ };
386
534
  /**
387
535
  * Add a GROUP BY clause. Multiple columns are comma-separated.
388
536
  * Combine with .aggregate() to get grouped aggregate results.
389
537
  */
390
- groupBy(...cols: (keyof T & string)[]): SelectBuilder<T, S>;
538
+ groupBy(...cols: (keyof T & string)[]): SelectBuilder<T, S, TRelations>;
391
539
  /**
392
540
  * Add a HAVING clause — filters aggregate groups.
393
541
  * Uses the same WhereInput system as .where() (supports operators, OR/AND).
@@ -395,7 +543,7 @@ declare class SelectBuilder<T, S extends SchemaMap> {
395
543
  * @example
396
544
  * .groupBy('role').aggregate({ cnt: { fn: 'COUNT' } }).having({ cnt: { op: '>', value: 1 } })
397
545
  */
398
- having(conditions: WhereInput<Record<string, unknown>>): SelectBuilder<T, S>;
546
+ having(conditions: WhereInput<Record<string, unknown>>): SelectBuilder<T, S, TRelations>;
399
547
  /**
400
548
  * Execute a GROUP BY + aggregate query.
401
549
  * Returns typed rows with group-by columns + aggregate aliases.
@@ -426,8 +574,133 @@ declare class SelectBuilder<T, S extends SchemaMap> {
426
574
  private _scalarAggregateRaw;
427
575
  select(): Promise<T[]>;
428
576
  first(): Promise<T | null>;
577
+ private _executeWith;
578
+ private _attachBelongsTo;
579
+ private _attachHasMany;
429
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[]>;
430
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>;
431
704
  }
432
705
  declare class InsertBuilder<T, S extends SchemaMap> {
433
706
  private readonly adapter;
@@ -435,9 +708,24 @@ declare class InsertBuilder<T, S extends SchemaMap> {
435
708
  private readonly ctx;
436
709
  private readonly queue;
437
710
  private readonly table;
438
- constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S>);
711
+ private readonly dialect;
712
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S>, dialect?: SqlDialect);
439
713
  insert(data: InferInsert<S>): Promise<T>;
440
- /** Serialize values for SQLite storage. Date → ISO string. */
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. */
441
729
  private _serializeForInsert;
442
730
  }
443
731
  declare class JoinBuilder {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oakbun/ws",
3
- "version": "0.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "WebSocket plugin for the OakBun framework.",
5
5
  "author": "René (SchildW3rk)",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "prepublishOnly": "bun run build"
42
42
  },
43
43
  "peerDependencies": {
44
- "oakbun": ">=0.1.1",
44
+ "oakbun": ">=0.3.0",
45
45
  "zod": "^3.0.0 || ^4.0.0"
46
46
  },
47
47
  "devDependencies": {