oakbun 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.js +1 -1
- package/dist/db/index.d.ts +238 -26
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/migrations/diff.d.ts +1 -1
- package/dist/db/migrations/diff.d.ts.map +1 -1
- package/dist/db/migrations/generator.d.ts +1 -1
- package/dist/db/migrations/generator.d.ts.map +1 -1
- package/dist/db/sql.d.ts +70 -2
- package/dist/db/sql.d.ts.map +1 -1
- package/dist/{db-XTXH6OKV.js → db-YSUNURBB.js} +706 -45
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +900 -146
- package/dist/index.js.map +1 -1
- package/dist/schema/table.d.ts +108 -4
- package/dist/schema/table.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -335,6 +335,14 @@ var init_types = __esm({
|
|
|
335
335
|
});
|
|
336
336
|
|
|
337
337
|
// src/db/sql.ts
|
|
338
|
+
function buildSubquery(sql, params, col) {
|
|
339
|
+
if (!sql) throw new Error("buildSubquery: sql must not be empty");
|
|
340
|
+
return {
|
|
341
|
+
_sql: `(${sql})`,
|
|
342
|
+
_params: params,
|
|
343
|
+
_phantom: { col, type: void 0 }
|
|
344
|
+
};
|
|
345
|
+
}
|
|
338
346
|
function validateAndQuoteOnClause(on) {
|
|
339
347
|
const trimmed = on.trim();
|
|
340
348
|
const match = ON_CLAUSE_PATTERN.exec(trimmed);
|
|
@@ -356,6 +364,9 @@ function filterDefined(data) {
|
|
|
356
364
|
function isWhereOp(val) {
|
|
357
365
|
return typeof val === "object" && val !== null && "op" in val && typeof val["op"] === "string";
|
|
358
366
|
}
|
|
367
|
+
function isSubqueryResult(val) {
|
|
368
|
+
return typeof val === "object" && val !== null && "_sql" in val && "_params" in val;
|
|
369
|
+
}
|
|
359
370
|
function buildFieldCondition(key, condition, dialect) {
|
|
360
371
|
if (!isWhereOp(condition)) {
|
|
361
372
|
return { sql: `"${key}" = ?`, params: [condition] };
|
|
@@ -375,7 +386,11 @@ function buildFieldCondition(key, condition, dialect) {
|
|
|
375
386
|
case "<=":
|
|
376
387
|
return { sql: `"${key}" <= ?`, params: [op.value] };
|
|
377
388
|
case "IN": {
|
|
378
|
-
const
|
|
389
|
+
const inVal = op.value;
|
|
390
|
+
if (isSubqueryResult(inVal)) {
|
|
391
|
+
return { sql: `"${key}" IN ${inVal._sql}`, params: inVal._params };
|
|
392
|
+
}
|
|
393
|
+
const vals = inVal;
|
|
379
394
|
if (vals.length === 0) {
|
|
380
395
|
return { sql: "1 = 0", params: [] };
|
|
381
396
|
}
|
|
@@ -383,7 +398,11 @@ function buildFieldCondition(key, condition, dialect) {
|
|
|
383
398
|
return { sql: `"${key}" IN (${placeholders})`, params: vals };
|
|
384
399
|
}
|
|
385
400
|
case "NOT IN": {
|
|
386
|
-
const
|
|
401
|
+
const inVal = op.value;
|
|
402
|
+
if (isSubqueryResult(inVal)) {
|
|
403
|
+
return { sql: `"${key}" NOT IN ${inVal._sql}`, params: inVal._params };
|
|
404
|
+
}
|
|
405
|
+
const vals = inVal;
|
|
387
406
|
if (vals.length === 0) {
|
|
388
407
|
return { sql: "1 = 1", params: [] };
|
|
389
408
|
}
|
|
@@ -454,6 +473,37 @@ function buildInsert(tableName, data, returning = true) {
|
|
|
454
473
|
const sql = `INSERT INTO "${tableName}" (${cols}) VALUES (${placeholders})${returning_clause}`;
|
|
455
474
|
return { sql, params };
|
|
456
475
|
}
|
|
476
|
+
function buildInsertMany(tableName, rows, returning = true) {
|
|
477
|
+
if (rows.length === 0) {
|
|
478
|
+
throw new Error("insertMany: rows array must not be empty");
|
|
479
|
+
}
|
|
480
|
+
const columns = Object.keys(rows[0]);
|
|
481
|
+
const quotedCols = columns.map((c) => `"${c}"`).join(", ");
|
|
482
|
+
const params = [];
|
|
483
|
+
const valueClauses = [];
|
|
484
|
+
for (const row of rows) {
|
|
485
|
+
const placeholders = [];
|
|
486
|
+
for (const col of columns) {
|
|
487
|
+
const val = row[col];
|
|
488
|
+
if (val === void 0) {
|
|
489
|
+
throw new Error(`insertMany: column "${col}" has undefined value \u2014 apply defaults before calling buildInsertMany`);
|
|
490
|
+
}
|
|
491
|
+
params.push(val);
|
|
492
|
+
placeholders.push("?");
|
|
493
|
+
}
|
|
494
|
+
valueClauses.push(`(${placeholders.join(", ")})`);
|
|
495
|
+
}
|
|
496
|
+
const returning_clause = returning ? " RETURNING *" : "";
|
|
497
|
+
const sql = `INSERT INTO "${tableName}" (${quotedCols}) VALUES ${valueClauses.join(", ")}${returning_clause}`;
|
|
498
|
+
return { sql, params };
|
|
499
|
+
}
|
|
500
|
+
function buildSoftDeleteUpdate(tableName, col, value, where, dialect = "sqlite") {
|
|
501
|
+
const serialized = value instanceof Date ? value.toISOString() : null;
|
|
502
|
+
const { sql: whereSql, params: whereParams } = buildWhere(where, dialect);
|
|
503
|
+
const setSql = `"${col}" = ?`;
|
|
504
|
+
const sql = whereSql ? `UPDATE "${tableName}" SET ${setSql} WHERE ${whereSql}` : `UPDATE "${tableName}" SET ${setSql}`;
|
|
505
|
+
return { sql, params: [serialized, ...whereParams] };
|
|
506
|
+
}
|
|
457
507
|
function buildUpdate(tableName, patch, pk, pkValue) {
|
|
458
508
|
const entries = filterDefined(patch);
|
|
459
509
|
const sets = entries.map(([key]) => `"${key}" = ?`).join(", ");
|
|
@@ -509,11 +559,27 @@ function appendPaginationAndOrder(parts, options) {
|
|
|
509
559
|
}
|
|
510
560
|
}
|
|
511
561
|
}
|
|
562
|
+
function buildUnion(parts, kind, options = {}) {
|
|
563
|
+
if (parts.length < 2) {
|
|
564
|
+
throw new Error("buildUnion: at least 2 parts required");
|
|
565
|
+
}
|
|
566
|
+
const sqls = parts.map((p) => p.sql);
|
|
567
|
+
const params = parts.flatMap((p) => p.params);
|
|
568
|
+
let sql = sqls.join(` ${kind} `);
|
|
569
|
+
if (options.orderBy) {
|
|
570
|
+
sql += ` ORDER BY "${options.orderBy.col}" ${options.orderBy.dir ?? "ASC"}`;
|
|
571
|
+
}
|
|
572
|
+
if (options.limit !== void 0) {
|
|
573
|
+
sql += ` LIMIT ${Math.trunc(Math.max(0, options.limit))}`;
|
|
574
|
+
}
|
|
575
|
+
return { sql, params };
|
|
576
|
+
}
|
|
512
577
|
function buildSelect(tableName, conditions, options, dialect = "sqlite") {
|
|
513
578
|
const selectList = buildSelectListFromOptions(options);
|
|
514
579
|
const { sql: whereSql, params } = buildWhere(conditions, dialect);
|
|
580
|
+
const selectKeyword = options?.distinct ? "SELECT DISTINCT" : "SELECT";
|
|
515
581
|
const parts = [
|
|
516
|
-
whereSql ?
|
|
582
|
+
whereSql ? `${selectKeyword} ${selectList} FROM "${tableName}" WHERE ${whereSql}` : `${selectKeyword} ${selectList} FROM "${tableName}"`
|
|
517
583
|
];
|
|
518
584
|
if (options?.groupBy && options.groupBy.length > 0) {
|
|
519
585
|
parts.push(`GROUP BY ${options.groupBy.map((c) => `"${c}"`).join(", ")}`);
|
|
@@ -564,9 +630,12 @@ var init_sql = __esm({
|
|
|
564
630
|
var db_exports = {};
|
|
565
631
|
__export(db_exports, {
|
|
566
632
|
BoundVelnDB: () => BoundVelnDB,
|
|
633
|
+
ColumnRestrictedBuilder: () => ColumnRestrictedBuilder,
|
|
567
634
|
InsertBuilder: () => InsertBuilder,
|
|
568
635
|
JoinBuilder: () => JoinBuilder,
|
|
569
636
|
SelectBuilder: () => SelectBuilder,
|
|
637
|
+
SoftDeleteBuilder: () => SoftDeleteBuilder,
|
|
638
|
+
UnionBuilder: () => UnionBuilder,
|
|
570
639
|
VelnDB: () => VelnDB
|
|
571
640
|
});
|
|
572
641
|
function mergeWhereAnd(a, b) {
|
|
@@ -577,7 +646,7 @@ function mergeWhereAnd(a, b) {
|
|
|
577
646
|
}
|
|
578
647
|
return { AND: [a, b] };
|
|
579
648
|
}
|
|
580
|
-
var VelnDB, BoundVelnDB, SelectBuilder, InsertBuilder, JoinBuilder;
|
|
649
|
+
var VelnDB, BoundVelnDB, SelectBuilder, ColumnRestrictedBuilder, SoftDeleteBuilder, UnionBuilder, InsertBuilder, JoinBuilder;
|
|
581
650
|
var init_db = __esm({
|
|
582
651
|
"src/db/index.ts"() {
|
|
583
652
|
"use strict";
|
|
@@ -597,10 +666,11 @@ var init_db = __esm({
|
|
|
597
666
|
}
|
|
598
667
|
};
|
|
599
668
|
BoundVelnDB = class _BoundVelnDB {
|
|
600
|
-
constructor(adapter, hooks, ctx, queue, queryLog) {
|
|
669
|
+
constructor(adapter, hooks, ctx, queue, queryLog, dialect = "sqlite") {
|
|
601
670
|
this.hooks = hooks;
|
|
602
671
|
this.ctx = ctx;
|
|
603
672
|
this.queue = queue;
|
|
673
|
+
this.dialect = dialect;
|
|
604
674
|
if (queryLog) {
|
|
605
675
|
const log = queryLog;
|
|
606
676
|
this.adapter = {
|
|
@@ -635,9 +705,11 @@ var init_db = __esm({
|
|
|
635
705
|
hooks;
|
|
636
706
|
ctx;
|
|
637
707
|
queue;
|
|
708
|
+
dialect;
|
|
638
709
|
/** Per-request query counter — incremented for every query() and execute() call on this instance. */
|
|
639
710
|
_queryCount = 0;
|
|
640
711
|
adapter;
|
|
712
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
641
713
|
from(table) {
|
|
642
714
|
return new SelectBuilder(this.adapter, this.hooks, this.ctx, this.queue, table, {});
|
|
643
715
|
}
|
|
@@ -657,20 +729,20 @@ var init_db = __esm({
|
|
|
657
729
|
return new JoinBuilder(this.adapter, tableName, [], [], "", []);
|
|
658
730
|
}
|
|
659
731
|
into(table) {
|
|
660
|
-
return new InsertBuilder(this.adapter, this.hooks, this.ctx, this.queue, table);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
732
|
+
return new InsertBuilder(this.adapter, this.hooks, this.ctx, this.queue, table, this.dialect);
|
|
733
|
+
}
|
|
734
|
+
// Implementation
|
|
735
|
+
async loadRelation(parents, keyOrRelationName, tableOrSource, primaryKey) {
|
|
736
|
+
if (primaryKey === void 0) {
|
|
737
|
+
return this._loadRelationByName(
|
|
738
|
+
parents,
|
|
739
|
+
keyOrRelationName,
|
|
740
|
+
tableOrSource,
|
|
741
|
+
"many"
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
const foreignKey = keyOrRelationName;
|
|
745
|
+
const childTable = tableOrSource;
|
|
674
746
|
const result = /* @__PURE__ */ new Map();
|
|
675
747
|
if (parents.length === 0) return result;
|
|
676
748
|
const ids = [...new Set(parents.map((p) => p[foreignKey]))];
|
|
@@ -686,15 +758,18 @@ var init_db = __esm({
|
|
|
686
758
|
}
|
|
687
759
|
return result;
|
|
688
760
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
761
|
+
// Implementation
|
|
762
|
+
async loadRelationOne(parents, keyOrRelationName, tableOrSource, primaryKey) {
|
|
763
|
+
if (primaryKey === void 0) {
|
|
764
|
+
return this._loadRelationByName(
|
|
765
|
+
parents,
|
|
766
|
+
keyOrRelationName,
|
|
767
|
+
tableOrSource,
|
|
768
|
+
"one"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
const foreignKey = keyOrRelationName;
|
|
772
|
+
const childTable = tableOrSource;
|
|
698
773
|
const result = /* @__PURE__ */ new Map();
|
|
699
774
|
if (parents.length === 0) return result;
|
|
700
775
|
const ids = [...new Set(parents.map((p) => p[foreignKey]))];
|
|
@@ -704,6 +779,55 @@ var init_db = __esm({
|
|
|
704
779
|
}
|
|
705
780
|
return result;
|
|
706
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* Shared implementation for name-based loadRelation / loadRelationOne.
|
|
784
|
+
* Reads RelationMeta from sourceTable.relations, validates the kind,
|
|
785
|
+
* and delegates to the explicit overload.
|
|
786
|
+
*/
|
|
787
|
+
async _loadRelationByName(parents, relationName, sourceTable, mode) {
|
|
788
|
+
const rel = sourceTable.relations[relationName];
|
|
789
|
+
if (rel === void 0) {
|
|
790
|
+
const available = Object.keys(sourceTable.relations).join(", ") || "(none)";
|
|
791
|
+
throw new Error(
|
|
792
|
+
`Relation '${relationName}' is not defined on table '${sourceTable.name}'. Available relations: ${available}`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if (rel.kind === "manyToMany") {
|
|
796
|
+
throw new Error(
|
|
797
|
+
`manyToMany relations are not yet supported in loadRelation. Use a manual JOIN for relation '${relationName}' on table '${sourceTable.name}'.`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
if (mode === "one" && rel.kind === "hasMany") {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`loadRelationOne cannot be used with hasMany relation '${relationName}' on table '${sourceTable.name}'. Use loadRelation to get an array of results.`
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
const foreignTable = rel.getTable();
|
|
806
|
+
const pk = foreignTable.primaryKey;
|
|
807
|
+
if (rel.kind === "belongsTo") {
|
|
808
|
+
const ft = foreignTable;
|
|
809
|
+
const fk = rel.foreignKey;
|
|
810
|
+
if (mode === "one") {
|
|
811
|
+
return this.loadRelationOne(parents, fk, ft, pk);
|
|
812
|
+
}
|
|
813
|
+
return this.loadRelation(parents, fk, ft, pk);
|
|
814
|
+
}
|
|
815
|
+
const parentPk = sourceTable.primaryKey;
|
|
816
|
+
const result = /* @__PURE__ */ new Map();
|
|
817
|
+
if (parents.length === 0) return result;
|
|
818
|
+
const ids = [...new Set(parents.map((p) => p[parentPk]))];
|
|
819
|
+
const children = await this.from(foreignTable).where({ [rel.foreignKey]: { op: "IN", value: ids } }).select();
|
|
820
|
+
for (const child of children) {
|
|
821
|
+
const key = child[rel.foreignKey];
|
|
822
|
+
const group = result.get(key);
|
|
823
|
+
if (group) {
|
|
824
|
+
group.push(child);
|
|
825
|
+
} else {
|
|
826
|
+
result.set(key, [child]);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
707
831
|
async transaction(fn) {
|
|
708
832
|
const txQueue = new RequestEventQueue();
|
|
709
833
|
const result = await this.adapter.transaction(async (txAdapter) => {
|
|
@@ -740,7 +864,7 @@ var init_db = __esm({
|
|
|
740
864
|
}
|
|
741
865
|
};
|
|
742
866
|
SelectBuilder = class _SelectBuilder {
|
|
743
|
-
constructor(adapter, hooks, ctx, queue, table, conditions, _options = {}, _rawWhere = [], _dialect = "sqlite") {
|
|
867
|
+
constructor(adapter, hooks, ctx, queue, table, conditions, _options = {}, _rawWhere = [], _dialect = "sqlite", _withRelations = [], _includeDeleted = false) {
|
|
744
868
|
this.adapter = adapter;
|
|
745
869
|
this.hooks = hooks;
|
|
746
870
|
this.ctx = ctx;
|
|
@@ -750,6 +874,8 @@ var init_db = __esm({
|
|
|
750
874
|
this._options = _options;
|
|
751
875
|
this._rawWhere = _rawWhere;
|
|
752
876
|
this._dialect = _dialect;
|
|
877
|
+
this._withRelations = _withRelations;
|
|
878
|
+
this._includeDeleted = _includeDeleted;
|
|
753
879
|
}
|
|
754
880
|
adapter;
|
|
755
881
|
hooks;
|
|
@@ -760,6 +886,8 @@ var init_db = __esm({
|
|
|
760
886
|
_options;
|
|
761
887
|
_rawWhere;
|
|
762
888
|
_dialect;
|
|
889
|
+
_withRelations;
|
|
890
|
+
_includeDeleted;
|
|
763
891
|
_cloneWith(conditions, rawWhere) {
|
|
764
892
|
return new _SelectBuilder(
|
|
765
893
|
this.adapter,
|
|
@@ -770,7 +898,9 @@ var init_db = __esm({
|
|
|
770
898
|
conditions,
|
|
771
899
|
this._options,
|
|
772
900
|
rawWhere ?? this._rawWhere,
|
|
773
|
-
this._dialect
|
|
901
|
+
this._dialect,
|
|
902
|
+
this._withRelations,
|
|
903
|
+
this._includeDeleted
|
|
774
904
|
);
|
|
775
905
|
}
|
|
776
906
|
_clone(patch) {
|
|
@@ -783,7 +913,60 @@ var init_db = __esm({
|
|
|
783
913
|
this.conditions,
|
|
784
914
|
{ ...this._options, ...patch },
|
|
785
915
|
this._rawWhere,
|
|
786
|
-
this._dialect
|
|
916
|
+
this._dialect,
|
|
917
|
+
this._withRelations,
|
|
918
|
+
this._includeDeleted
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Eager-load relations alongside the main query.
|
|
923
|
+
* One additional IN-query per relation — never N+1 regardless of row count.
|
|
924
|
+
*
|
|
925
|
+
* @example
|
|
926
|
+
* const posts = await db.from(postsTable).with({ author: true }).select()
|
|
927
|
+
* posts[0].author // → User | null (fully typed)
|
|
928
|
+
* posts[0].title // → string (original fields preserved)
|
|
929
|
+
*/
|
|
930
|
+
with(relations) {
|
|
931
|
+
const keys = Object.keys(relations);
|
|
932
|
+
return new _SelectBuilder(
|
|
933
|
+
this.adapter,
|
|
934
|
+
this.hooks,
|
|
935
|
+
this.ctx,
|
|
936
|
+
this.queue,
|
|
937
|
+
// table type cast: the schema/relations are unchanged; only T changes in the generic
|
|
938
|
+
this.table,
|
|
939
|
+
this.conditions,
|
|
940
|
+
this._options,
|
|
941
|
+
this._rawWhere,
|
|
942
|
+
this._dialect,
|
|
943
|
+
[...this._withRelations, ...keys],
|
|
944
|
+
this._includeDeleted
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Include soft-deleted rows in the query result.
|
|
949
|
+
* By default, tables with `.withSoftDelete()` automatically exclude rows
|
|
950
|
+
* where the soft-delete column is not null.
|
|
951
|
+
*
|
|
952
|
+
* Has no effect on tables without soft delete configured.
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* const allUsers = await db.from(usersTable).withDeleted().select()
|
|
956
|
+
*/
|
|
957
|
+
withDeleted() {
|
|
958
|
+
return new _SelectBuilder(
|
|
959
|
+
this.adapter,
|
|
960
|
+
this.hooks,
|
|
961
|
+
this.ctx,
|
|
962
|
+
this.queue,
|
|
963
|
+
this.table,
|
|
964
|
+
this.conditions,
|
|
965
|
+
this._options,
|
|
966
|
+
this._rawWhere,
|
|
967
|
+
this._dialect,
|
|
968
|
+
this._withRelations,
|
|
969
|
+
true
|
|
787
970
|
);
|
|
788
971
|
}
|
|
789
972
|
/**
|
|
@@ -810,6 +993,17 @@ var init_db = __esm({
|
|
|
810
993
|
whereRaw(sql, params) {
|
|
811
994
|
return this._cloneWith(this.conditions, [...this._rawWhere, { sql, params }]);
|
|
812
995
|
}
|
|
996
|
+
/**
|
|
997
|
+
* Apply SELECT DISTINCT — deduplicate rows in the result set.
|
|
998
|
+
* Combine with `.columns()` to deduplicate on specific columns.
|
|
999
|
+
*
|
|
1000
|
+
* @example
|
|
1001
|
+
* await db.from(usersTable).columns('name').distinct().select()
|
|
1002
|
+
* // → SELECT DISTINCT "name" FROM "users"
|
|
1003
|
+
*/
|
|
1004
|
+
distinct() {
|
|
1005
|
+
return this._clone({ distinct: true });
|
|
1006
|
+
}
|
|
813
1007
|
/** Limit the number of rows returned. Bound as a parameter — never interpolated. */
|
|
814
1008
|
limit(n) {
|
|
815
1009
|
return this._clone({ limit: n });
|
|
@@ -831,14 +1025,74 @@ var init_db = __esm({
|
|
|
831
1025
|
page(page, size) {
|
|
832
1026
|
return this._clone({ limit: size, offset: (page - 1) * size });
|
|
833
1027
|
}
|
|
1028
|
+
columns(...cols) {
|
|
1029
|
+
const cloned = this._clone({ columns: cols });
|
|
1030
|
+
if (cols.length === 1) {
|
|
1031
|
+
return new ColumnRestrictedBuilder(cloned, cols[0]);
|
|
1032
|
+
}
|
|
1033
|
+
return cloned;
|
|
1034
|
+
}
|
|
834
1035
|
/**
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
* Return type is narrowed to Pick<T, K> for full type safety.
|
|
1036
|
+
* Build SELECT SQL + params without executing the query.
|
|
1037
|
+
* Used internally by ColumnRestrictedBuilder.subquery().
|
|
839
1038
|
*/
|
|
840
|
-
|
|
841
|
-
|
|
1039
|
+
/** Internal accessor for ColumnRestrictedBuilder / UnionBuilder — returns the adapter. */
|
|
1040
|
+
_getAdapter() {
|
|
1041
|
+
return this.adapter;
|
|
1042
|
+
}
|
|
1043
|
+
/** Internal accessor for ColumnRestrictedBuilder / UnionBuilder — returns the SQL dialect. */
|
|
1044
|
+
_getDialect() {
|
|
1045
|
+
return this._dialect;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Returns the effective WHERE conditions, injecting the soft-delete IS NULL
|
|
1049
|
+
* filter when the table has a soft-delete column and .withDeleted() was not called.
|
|
1050
|
+
*/
|
|
1051
|
+
_effectiveConditions() {
|
|
1052
|
+
const col = this.table.softDeleteColumn;
|
|
1053
|
+
if (col === null || this._includeDeleted) return this.conditions;
|
|
1054
|
+
const softFilter = { [col]: { op: "IS NULL" } };
|
|
1055
|
+
return mergeWhereAnd(this.conditions, softFilter);
|
|
1056
|
+
}
|
|
1057
|
+
_buildSelectSQL() {
|
|
1058
|
+
const conditions = this._effectiveConditions();
|
|
1059
|
+
if (this._rawWhere.length === 0) {
|
|
1060
|
+
return buildSelect(
|
|
1061
|
+
this.table.name,
|
|
1062
|
+
conditions,
|
|
1063
|
+
this._options,
|
|
1064
|
+
this._dialect
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const { sql: whereSql, params: whereParams } = buildWhere(
|
|
1068
|
+
conditions,
|
|
1069
|
+
this._dialect
|
|
1070
|
+
);
|
|
1071
|
+
const allWhereParts = [];
|
|
1072
|
+
const allParams = [...whereParams];
|
|
1073
|
+
if (whereSql) allWhereParts.push(whereSql);
|
|
1074
|
+
for (const raw of this._rawWhere) {
|
|
1075
|
+
allWhereParts.push(raw.sql);
|
|
1076
|
+
allParams.push(...raw.params);
|
|
1077
|
+
}
|
|
1078
|
+
const combinedWhere = allWhereParts.join(" AND ");
|
|
1079
|
+
const selectList = buildSelectListFromOptions(this._options);
|
|
1080
|
+
const selectKeyword = this._options.distinct ? "SELECT DISTINCT" : "SELECT";
|
|
1081
|
+
const parts = [
|
|
1082
|
+
combinedWhere ? `${selectKeyword} ${selectList} FROM "${this.table.name}" WHERE ${combinedWhere}` : `${selectKeyword} ${selectList} FROM "${this.table.name}"`
|
|
1083
|
+
];
|
|
1084
|
+
if (this._options.orderBy && this._options.orderBy.length > 0) {
|
|
1085
|
+
const clause = this._options.orderBy.map(({ col, dir }) => `"${col}" ${dir}`).join(", ");
|
|
1086
|
+
parts.push(`ORDER BY ${clause}`);
|
|
1087
|
+
}
|
|
1088
|
+
if (this._options.limit !== void 0 || this._options.offset !== void 0) {
|
|
1089
|
+
const limitVal = this._options.limit !== void 0 ? Math.trunc(Math.max(0, this._options.limit)) : -1;
|
|
1090
|
+
parts.push(`LIMIT ${limitVal}`);
|
|
1091
|
+
if (this._options.offset !== void 0) {
|
|
1092
|
+
parts.push(`OFFSET ${Math.trunc(Math.max(0, this._options.offset))}`);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { sql: parts.join(" "), params: allParams };
|
|
842
1096
|
}
|
|
843
1097
|
/**
|
|
844
1098
|
* Add a GROUP BY clause. Multiple columns are comma-separated.
|
|
@@ -875,7 +1129,7 @@ var init_db = __esm({
|
|
|
875
1129
|
}));
|
|
876
1130
|
const { sql, params } = buildSelect(
|
|
877
1131
|
this.table.name,
|
|
878
|
-
this.
|
|
1132
|
+
this._effectiveConditions(),
|
|
879
1133
|
{ ...this._options, aggregates: aggClauses },
|
|
880
1134
|
this._dialect
|
|
881
1135
|
);
|
|
@@ -913,7 +1167,7 @@ var init_db = __esm({
|
|
|
913
1167
|
const alias = "_agg";
|
|
914
1168
|
const colExpr = col ? `"${col}"` : "*";
|
|
915
1169
|
const { sql: whereSql, params } = buildWhere(
|
|
916
|
-
this.
|
|
1170
|
+
this._effectiveConditions(),
|
|
917
1171
|
this._dialect
|
|
918
1172
|
);
|
|
919
1173
|
let sqlStr;
|
|
@@ -939,10 +1193,11 @@ var init_db = __esm({
|
|
|
939
1193
|
async select() {
|
|
940
1194
|
let finalSql;
|
|
941
1195
|
let finalParams;
|
|
1196
|
+
const effectiveConditions = this._effectiveConditions();
|
|
942
1197
|
if (this._rawWhere.length === 0) {
|
|
943
1198
|
const { sql, params } = buildSelect(
|
|
944
1199
|
this.table.name,
|
|
945
|
-
|
|
1200
|
+
effectiveConditions,
|
|
946
1201
|
this._options,
|
|
947
1202
|
this._dialect
|
|
948
1203
|
);
|
|
@@ -950,7 +1205,7 @@ var init_db = __esm({
|
|
|
950
1205
|
finalParams = params;
|
|
951
1206
|
} else {
|
|
952
1207
|
const { sql: whereSql, params: whereParams } = buildWhere(
|
|
953
|
-
|
|
1208
|
+
effectiveConditions,
|
|
954
1209
|
this._dialect
|
|
955
1210
|
);
|
|
956
1211
|
const allWhereParts = [];
|
|
@@ -989,16 +1244,103 @@ var init_db = __esm({
|
|
|
989
1244
|
finalSql = parts.join(" ");
|
|
990
1245
|
finalParams = allParams;
|
|
991
1246
|
}
|
|
992
|
-
const
|
|
1247
|
+
const rawRows = await this.adapter.query(finalSql, finalParams);
|
|
1248
|
+
let rows;
|
|
993
1249
|
if (this._options.columns && this._options.columns.length > 0) {
|
|
994
|
-
|
|
1250
|
+
rows = rawRows;
|
|
1251
|
+
} else {
|
|
1252
|
+
rows = rawRows.map((row) => deserializeRow(this.table, row));
|
|
995
1253
|
}
|
|
996
|
-
|
|
1254
|
+
if (this._withRelations.length === 0) return rows;
|
|
1255
|
+
return this._executeWith(rows);
|
|
997
1256
|
}
|
|
998
1257
|
async first() {
|
|
999
1258
|
const rows = await this.select();
|
|
1000
1259
|
return rows[0] ?? null;
|
|
1001
1260
|
}
|
|
1261
|
+
// ── Eager loading — _executeWith ────────────────────────────────────────
|
|
1262
|
+
async _executeWith(rows) {
|
|
1263
|
+
if (rows.length === 0) return rows;
|
|
1264
|
+
const mutableRows = rows.map((r) => ({ ...r }));
|
|
1265
|
+
for (const relationName of this._withRelations) {
|
|
1266
|
+
const meta = this.table.relations[relationName];
|
|
1267
|
+
if (!meta) continue;
|
|
1268
|
+
if (meta.kind === "manyToMany") {
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`manyToMany eager loading is not yet supported. Use loadRelation manually for relation '${relationName}'.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
if (meta.kind === "belongsTo") {
|
|
1274
|
+
await this._attachBelongsTo(mutableRows, relationName, meta);
|
|
1275
|
+
} else if (meta.kind === "hasMany") {
|
|
1276
|
+
await this._attachHasMany(mutableRows, relationName, meta);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return mutableRows;
|
|
1280
|
+
}
|
|
1281
|
+
async _attachBelongsTo(rows, relationName, meta) {
|
|
1282
|
+
const foreignTable = meta.getTable();
|
|
1283
|
+
const fkValues = rows.map((r) => r[meta.foreignKey]).filter((v) => v !== null && v !== void 0);
|
|
1284
|
+
if (fkValues.length === 0) {
|
|
1285
|
+
for (const r of rows) r[relationName] = null;
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
const uniqueFkValues = [...new Set(fkValues)];
|
|
1289
|
+
const pk = foreignTable.primaryKey;
|
|
1290
|
+
const baseConditions = { [pk]: { op: "IN", value: uniqueFkValues } };
|
|
1291
|
+
if (foreignTable.softDeleteColumn !== null) {
|
|
1292
|
+
baseConditions[foreignTable.softDeleteColumn] = { op: "IS NULL" };
|
|
1293
|
+
}
|
|
1294
|
+
const { sql, params } = buildSelect(
|
|
1295
|
+
foreignTable.name,
|
|
1296
|
+
baseConditions,
|
|
1297
|
+
{},
|
|
1298
|
+
this._dialect
|
|
1299
|
+
);
|
|
1300
|
+
const related = await this.adapter.query(sql, params);
|
|
1301
|
+
const relatedMap = /* @__PURE__ */ new Map();
|
|
1302
|
+
for (const r of related) {
|
|
1303
|
+
relatedMap.set(r[pk], deserializeRow(foreignTable, r));
|
|
1304
|
+
}
|
|
1305
|
+
for (const r of rows) {
|
|
1306
|
+
r[relationName] = relatedMap.get(r[meta.foreignKey]) ?? null;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async _attachHasMany(rows, relationName, meta) {
|
|
1310
|
+
const foreignTable = meta.getTable();
|
|
1311
|
+
const pk = this.table.primaryKey;
|
|
1312
|
+
const pkValues = rows.map((r) => r[pk]).filter((v) => v !== null && v !== void 0);
|
|
1313
|
+
if (pkValues.length === 0) {
|
|
1314
|
+
for (const r of rows) r[relationName] = [];
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const hasManyConditions = { [meta.foreignKey]: { op: "IN", value: pkValues } };
|
|
1318
|
+
if (foreignTable.softDeleteColumn !== null) {
|
|
1319
|
+
hasManyConditions[foreignTable.softDeleteColumn] = { op: "IS NULL" };
|
|
1320
|
+
}
|
|
1321
|
+
const { sql, params } = buildSelect(
|
|
1322
|
+
foreignTable.name,
|
|
1323
|
+
hasManyConditions,
|
|
1324
|
+
{},
|
|
1325
|
+
this._dialect
|
|
1326
|
+
);
|
|
1327
|
+
const related = await this.adapter.query(sql, params);
|
|
1328
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1329
|
+
for (const pkVal of pkValues) grouped.set(pkVal, []);
|
|
1330
|
+
for (const r of related) {
|
|
1331
|
+
const fkVal = r[meta.foreignKey];
|
|
1332
|
+
const deserialized = deserializeRow(foreignTable, r);
|
|
1333
|
+
const group = grouped.get(fkVal);
|
|
1334
|
+
if (group) {
|
|
1335
|
+
group.push(deserialized);
|
|
1336
|
+
} else {
|
|
1337
|
+
grouped.set(fkVal, [deserialized]);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
for (const r of rows) {
|
|
1341
|
+
r[relationName] = grouped.get(r[pk]) ?? [];
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1002
1344
|
async update(patch) {
|
|
1003
1345
|
const hasConditions = !(Object.keys(this.conditions).length === 0 && this._rawWhere.length === 0);
|
|
1004
1346
|
if (!hasConditions) {
|
|
@@ -1026,6 +1368,58 @@ var init_db = __esm({
|
|
|
1026
1368
|
await this.hooks.runAfterUpdate(this.table, this.ctx, result, current, this.queue);
|
|
1027
1369
|
return result;
|
|
1028
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Update multiple rows atomically inside a single transaction.
|
|
1373
|
+
* Each row must include the primary key. beforeUpdate and afterUpdate hooks
|
|
1374
|
+
* run per row. If any row fails, the entire transaction rolls back.
|
|
1375
|
+
*
|
|
1376
|
+
* @example
|
|
1377
|
+
* const updated = await db.from(usersTable).updateMany([
|
|
1378
|
+
* { id: 1, name: 'Alice Updated' },
|
|
1379
|
+
* { id: 2, role: 'admin' },
|
|
1380
|
+
* ])
|
|
1381
|
+
*/
|
|
1382
|
+
async updateMany(rows) {
|
|
1383
|
+
if (rows.length === 0) return [];
|
|
1384
|
+
const pk = this.table.primaryKey;
|
|
1385
|
+
const results = await this.adapter.transaction(async (txAdapter) => {
|
|
1386
|
+
const txQueue = new RequestEventQueue();
|
|
1387
|
+
const inner = [];
|
|
1388
|
+
for (const row of rows) {
|
|
1389
|
+
const pkValue = row[pk];
|
|
1390
|
+
if (pkValue === void 0 || pkValue === null) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
`updateMany: row is missing primary key "${pk}" \u2014 every row must include the PK`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
const selectSql = `SELECT * FROM "${this.table.name}" WHERE "${pk}" = ?`;
|
|
1396
|
+
const currentRows = await txAdapter.query(selectSql, [pkValue]);
|
|
1397
|
+
if (currentRows.length === 0) {
|
|
1398
|
+
throw new Error(`updateMany: record with ${pk}=${String(pkValue)} not found`);
|
|
1399
|
+
}
|
|
1400
|
+
const current = deserializeRow(this.table, currentRows[0]);
|
|
1401
|
+
const { [pk]: _pk, ...patchWithoutPk } = row;
|
|
1402
|
+
const patch = patchWithoutPk;
|
|
1403
|
+
const finalPatch = await this.hooks.runBeforeUpdate(this.table, this.ctx, current, patch);
|
|
1404
|
+
const { sql, params } = buildUpdate(
|
|
1405
|
+
this.table.name,
|
|
1406
|
+
finalPatch,
|
|
1407
|
+
pk,
|
|
1408
|
+
pkValue
|
|
1409
|
+
);
|
|
1410
|
+
await txAdapter.execute(sql, params);
|
|
1411
|
+
const updatedRow = {
|
|
1412
|
+
...current,
|
|
1413
|
+
...finalPatch
|
|
1414
|
+
};
|
|
1415
|
+
const result = deserializeRow(this.table, updatedRow);
|
|
1416
|
+
await this.hooks.runAfterUpdate(this.table, this.ctx, result, current, txQueue);
|
|
1417
|
+
inner.push(result);
|
|
1418
|
+
}
|
|
1419
|
+
return inner;
|
|
1420
|
+
});
|
|
1421
|
+
return results;
|
|
1422
|
+
}
|
|
1029
1423
|
async delete() {
|
|
1030
1424
|
const hasConditions = !(Object.keys(this.conditions).length === 0 && this._rawWhere.length === 0);
|
|
1031
1425
|
if (!hasConditions) {
|
|
@@ -1043,20 +1437,235 @@ var init_db = __esm({
|
|
|
1043
1437
|
await this.hooks.runAfterDelete(this.table, this.ctx, current, this.queue);
|
|
1044
1438
|
return current;
|
|
1045
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Soft-delete rows by setting the soft-delete column to the current timestamp.
|
|
1442
|
+
* The table must have `.withSoftDelete()` configured — throws otherwise (at execute() time).
|
|
1443
|
+
*
|
|
1444
|
+
* Does NOT call beforeUpdate/afterUpdate hooks.
|
|
1445
|
+
* Without .where(), all rows in the table are soft-deleted.
|
|
1446
|
+
*
|
|
1447
|
+
* @example
|
|
1448
|
+
* await db.from(usersTable).softDelete().where({ id: 1 }).execute()
|
|
1449
|
+
*/
|
|
1450
|
+
softDelete() {
|
|
1451
|
+
return new SoftDeleteBuilder(this.adapter, this.table, /* @__PURE__ */ new Date(), this._dialect);
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Restore soft-deleted rows by setting the soft-delete column back to null.
|
|
1455
|
+
* The table must have `.withSoftDelete()` configured — throws otherwise (at execute() time).
|
|
1456
|
+
*
|
|
1457
|
+
* @example
|
|
1458
|
+
* await db.from(usersTable).restore().where({ id: 1 }).execute()
|
|
1459
|
+
*/
|
|
1460
|
+
restore() {
|
|
1461
|
+
return new SoftDeleteBuilder(this.adapter, this.table, null, this._dialect);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
ColumnRestrictedBuilder = class _ColumnRestrictedBuilder {
|
|
1465
|
+
// _builder is typed as SelectBuilder<unknown, ...> to avoid variance issues
|
|
1466
|
+
// when constructing from SelectBuilder<Pick<T,K>, ...> — the runtime shape is identical.
|
|
1467
|
+
constructor(_builder, _col) {
|
|
1468
|
+
this._builder = _builder;
|
|
1469
|
+
this._col = _col;
|
|
1470
|
+
}
|
|
1471
|
+
_builder;
|
|
1472
|
+
_col;
|
|
1473
|
+
where(conditions) {
|
|
1474
|
+
return new _ColumnRestrictedBuilder(
|
|
1475
|
+
this._builder.where(conditions),
|
|
1476
|
+
this._col
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
limit(n) {
|
|
1480
|
+
return new _ColumnRestrictedBuilder(
|
|
1481
|
+
this._builder.limit(n),
|
|
1482
|
+
this._col
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
orderBy(col, dir = "ASC") {
|
|
1486
|
+
return new _ColumnRestrictedBuilder(
|
|
1487
|
+
// _builder is SelectBuilder<unknown,...> so keyof unknown = never;
|
|
1488
|
+
// col is a valid schema key at runtime — cast is safe.
|
|
1489
|
+
this._builder.orderBy(col, dir),
|
|
1490
|
+
this._col
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Build the SQL for this query as a subquery fragment.
|
|
1495
|
+
* The result can be used directly in WHERE IN / NOT IN conditions.
|
|
1496
|
+
*
|
|
1497
|
+
* @example
|
|
1498
|
+
* const activeIds = db.from(usersTable).columns('id').where({ active: true }).subquery()
|
|
1499
|
+
* // → SubqueryResult<'id', number>
|
|
1500
|
+
*
|
|
1501
|
+
* const posts = await db.from(postsTable)
|
|
1502
|
+
* .where({ authorId: { op: 'IN', value: activeIds } })
|
|
1503
|
+
* .select()
|
|
1504
|
+
*/
|
|
1505
|
+
subquery() {
|
|
1506
|
+
const { sql, params } = this._builder._buildSelectSQL();
|
|
1507
|
+
return buildSubquery(sql, params, this._col);
|
|
1508
|
+
}
|
|
1509
|
+
/** Build raw SELECT SQL + params without parentheses (for UNION). */
|
|
1510
|
+
_buildRawSQL() {
|
|
1511
|
+
return this._builder._buildSelectSQL();
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Combine this query with another same-type column query via UNION (deduplicates).
|
|
1515
|
+
* Both sides must produce the same column type — enforced at compile time.
|
|
1516
|
+
*
|
|
1517
|
+
* @example
|
|
1518
|
+
* db.from(usersTable).columns('id')
|
|
1519
|
+
* .union(db.from(adminsTable).columns('id'))
|
|
1520
|
+
* .select()
|
|
1521
|
+
*/
|
|
1522
|
+
union(other) {
|
|
1523
|
+
return new UnionBuilder(
|
|
1524
|
+
[this._buildRawSQL(), other._buildRawSQL()],
|
|
1525
|
+
"UNION",
|
|
1526
|
+
this._builder._getAdapter(),
|
|
1527
|
+
this._builder._getDialect()
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Combine via UNION ALL — keeps duplicate rows.
|
|
1532
|
+
*/
|
|
1533
|
+
unionAll(other) {
|
|
1534
|
+
return new UnionBuilder(
|
|
1535
|
+
[this._buildRawSQL(), other._buildRawSQL()],
|
|
1536
|
+
"UNION ALL",
|
|
1537
|
+
this._builder._getAdapter(),
|
|
1538
|
+
this._builder._getDialect()
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
SoftDeleteBuilder = class {
|
|
1543
|
+
constructor(adapter, table, _value, _dialect = "sqlite") {
|
|
1544
|
+
this.adapter = adapter;
|
|
1545
|
+
this.table = table;
|
|
1546
|
+
this._value = _value;
|
|
1547
|
+
this._dialect = _dialect;
|
|
1548
|
+
}
|
|
1549
|
+
adapter;
|
|
1550
|
+
table;
|
|
1551
|
+
_value;
|
|
1552
|
+
_dialect;
|
|
1553
|
+
_conditions = {};
|
|
1554
|
+
/**
|
|
1555
|
+
* Add WHERE conditions to scope which rows are soft-deleted / restored.
|
|
1556
|
+
* Multiple calls accumulate with AND.
|
|
1557
|
+
* Without .where(), all rows in the table are affected.
|
|
1558
|
+
*/
|
|
1559
|
+
where(conditions) {
|
|
1560
|
+
this._conditions = mergeWhereAnd(this._conditions, conditions);
|
|
1561
|
+
return this;
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Execute the soft-delete or restore UPDATE.
|
|
1565
|
+
* Throws if the table has no softDeleteColumn configured.
|
|
1566
|
+
*/
|
|
1567
|
+
async execute() {
|
|
1568
|
+
const col = this.table.softDeleteColumn;
|
|
1569
|
+
if (col === null) {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`softDelete() called on table '${this.table.name}' which has no soft delete column. Add .withSoftDelete('deletedAt') to the table definition.`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
const { sql, params } = buildSoftDeleteUpdate(
|
|
1575
|
+
this.table.name,
|
|
1576
|
+
col,
|
|
1577
|
+
this._value,
|
|
1578
|
+
this._conditions,
|
|
1579
|
+
this._dialect
|
|
1580
|
+
);
|
|
1581
|
+
await this.adapter.execute(sql, params);
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
UnionBuilder = class _UnionBuilder {
|
|
1585
|
+
constructor(_parts, _kind, _adapter, _dialect) {
|
|
1586
|
+
this._parts = _parts;
|
|
1587
|
+
this._kind = _kind;
|
|
1588
|
+
this._adapter = _adapter;
|
|
1589
|
+
this._dialect = _dialect;
|
|
1590
|
+
}
|
|
1591
|
+
_parts;
|
|
1592
|
+
_kind;
|
|
1593
|
+
_adapter;
|
|
1594
|
+
_dialect;
|
|
1595
|
+
_orderBy;
|
|
1596
|
+
_limit;
|
|
1597
|
+
/** Append another UNION (deduplicating) leg. */
|
|
1598
|
+
union(other) {
|
|
1599
|
+
return new _UnionBuilder(
|
|
1600
|
+
[...this._parts, other._buildRawSQL()],
|
|
1601
|
+
"UNION",
|
|
1602
|
+
this._adapter,
|
|
1603
|
+
this._dialect
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
/** Append another UNION ALL (keep duplicates) leg. */
|
|
1607
|
+
unionAll(other) {
|
|
1608
|
+
return new _UnionBuilder(
|
|
1609
|
+
[...this._parts, other._buildRawSQL()],
|
|
1610
|
+
"UNION ALL",
|
|
1611
|
+
this._adapter,
|
|
1612
|
+
this._dialect
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
/** Add ORDER BY to the entire UNION result. */
|
|
1616
|
+
orderBy(col, dir = "ASC") {
|
|
1617
|
+
const next = new _UnionBuilder(this._parts, this._kind, this._adapter, this._dialect);
|
|
1618
|
+
next._orderBy = { col, dir };
|
|
1619
|
+
next._limit = this._limit;
|
|
1620
|
+
return next;
|
|
1621
|
+
}
|
|
1622
|
+
/** Add LIMIT to the entire UNION result. */
|
|
1623
|
+
limit(n) {
|
|
1624
|
+
const next = new _UnionBuilder(this._parts, this._kind, this._adapter, this._dialect);
|
|
1625
|
+
next._orderBy = this._orderBy;
|
|
1626
|
+
next._limit = n;
|
|
1627
|
+
return next;
|
|
1628
|
+
}
|
|
1629
|
+
/** Execute the UNION query and return typed rows. */
|
|
1630
|
+
async select() {
|
|
1631
|
+
const { sql, params } = buildUnion(this._parts, this._kind, {
|
|
1632
|
+
orderBy: this._orderBy,
|
|
1633
|
+
limit: this._limit
|
|
1634
|
+
});
|
|
1635
|
+
return this._adapter.query(sql, params);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Build the UNION as a subquery — wrapped in parentheses.
|
|
1639
|
+
* Usable in WHERE IN / NOT IN conditions.
|
|
1640
|
+
*
|
|
1641
|
+
* @example
|
|
1642
|
+
* const adminOrModIds = db.from(usersTable).columns('id').where({ role: 'admin' })
|
|
1643
|
+
* .union(db.from(usersTable).columns('id').where({ role: 'mod' }))
|
|
1644
|
+
* .subquery()
|
|
1645
|
+
*/
|
|
1646
|
+
subquery() {
|
|
1647
|
+
const { sql, params } = buildUnion(this._parts, this._kind, {
|
|
1648
|
+
orderBy: this._orderBy,
|
|
1649
|
+
limit: this._limit
|
|
1650
|
+
});
|
|
1651
|
+
return buildSubquery(sql, params, "");
|
|
1652
|
+
}
|
|
1046
1653
|
};
|
|
1047
1654
|
InsertBuilder = class {
|
|
1048
|
-
constructor(adapter, hooks, ctx, queue, table) {
|
|
1655
|
+
constructor(adapter, hooks, ctx, queue, table, dialect = "sqlite") {
|
|
1049
1656
|
this.adapter = adapter;
|
|
1050
1657
|
this.hooks = hooks;
|
|
1051
1658
|
this.ctx = ctx;
|
|
1052
1659
|
this.queue = queue;
|
|
1053
1660
|
this.table = table;
|
|
1661
|
+
this.dialect = dialect;
|
|
1054
1662
|
}
|
|
1055
1663
|
adapter;
|
|
1056
1664
|
hooks;
|
|
1057
1665
|
ctx;
|
|
1058
1666
|
queue;
|
|
1059
1667
|
table;
|
|
1668
|
+
dialect;
|
|
1060
1669
|
async insert(data) {
|
|
1061
1670
|
const originalInput = { ...data };
|
|
1062
1671
|
let current = { ...data };
|
|
@@ -1080,7 +1689,59 @@ var init_db = __esm({
|
|
|
1080
1689
|
await this.hooks.runAfterInsert(this.table, this.ctx, result, originalInput, this.queue);
|
|
1081
1690
|
return result;
|
|
1082
1691
|
}
|
|
1083
|
-
/**
|
|
1692
|
+
/**
|
|
1693
|
+
* Insert multiple rows in a single SQL statement.
|
|
1694
|
+
* beforeInsert and afterInsert hooks run per row.
|
|
1695
|
+
* Defaults (defaultFn / defaultValue) are applied per row.
|
|
1696
|
+
*
|
|
1697
|
+
* MySQL is not yet supported (no RETURNING *) — throws an informative error.
|
|
1698
|
+
*
|
|
1699
|
+
* @example
|
|
1700
|
+
* const users = await db.into(usersTable).insertMany([
|
|
1701
|
+
* { name: 'Alice', email: 'alice@example.com' },
|
|
1702
|
+
* { name: 'Bob', email: 'bob@example.com' },
|
|
1703
|
+
* ])
|
|
1704
|
+
*/
|
|
1705
|
+
async insertMany(data) {
|
|
1706
|
+
if (data.length === 0) return [];
|
|
1707
|
+
if (this.dialect === "mysql") {
|
|
1708
|
+
throw new Error(
|
|
1709
|
+
"insertMany is not yet supported for MySQL \u2014 MySQL does not support RETURNING *. Use individual insert() calls inside a transaction() instead."
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
const serializedRows = [];
|
|
1713
|
+
for (const row of data) {
|
|
1714
|
+
let processed = await this.hooks.runBeforeInsert(
|
|
1715
|
+
this.table,
|
|
1716
|
+
this.ctx,
|
|
1717
|
+
{ ...row }
|
|
1718
|
+
);
|
|
1719
|
+
for (const [field, col] of Object.entries(this.table.schema)) {
|
|
1720
|
+
if (col.def.primaryKey && col.def.autoIncrement) continue;
|
|
1721
|
+
if (processed[field] === void 0) {
|
|
1722
|
+
if (col.def.defaultFn !== void 0) {
|
|
1723
|
+
;
|
|
1724
|
+
processed[field] = col.def.defaultFn();
|
|
1725
|
+
} else if (col.def.defaultValue !== void 0) {
|
|
1726
|
+
;
|
|
1727
|
+
processed[field] = col.def.defaultValue;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const serialized = this._serializeForInsert(processed);
|
|
1732
|
+
serializedRows.push(serialized);
|
|
1733
|
+
}
|
|
1734
|
+
const { sql, params } = buildInsertMany(this.table.name, serializedRows, true);
|
|
1735
|
+
const rawRows = await this.adapter.query(sql, params);
|
|
1736
|
+
const results = [];
|
|
1737
|
+
for (const rawRow of rawRows) {
|
|
1738
|
+
const deserialized = deserializeRow(this.table, rawRow);
|
|
1739
|
+
await this.hooks.runAfterInsert(this.table, this.ctx, deserialized, {}, this.queue);
|
|
1740
|
+
results.push(deserialized);
|
|
1741
|
+
}
|
|
1742
|
+
return results;
|
|
1743
|
+
}
|
|
1744
|
+
/** Serialize values for storage. Date → ISO string. Drops undefined values. */
|
|
1084
1745
|
_serializeForInsert(data) {
|
|
1085
1746
|
const result = {};
|
|
1086
1747
|
for (const [key, val] of Object.entries(data)) {
|
|
@@ -1289,18 +1950,103 @@ var TableBuilder = class _TableBuilder {
|
|
|
1289
1950
|
_schema;
|
|
1290
1951
|
_hooks = [];
|
|
1291
1952
|
_events = {};
|
|
1953
|
+
_relations = {};
|
|
1954
|
+
_softDeleteColumn = null;
|
|
1292
1955
|
// Register table-level hook (no ctx)
|
|
1293
1956
|
hook(handlers) {
|
|
1294
1957
|
this._hooks.push(handlers);
|
|
1295
1958
|
return this;
|
|
1296
1959
|
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Designate a column as the soft-delete timestamp.
|
|
1962
|
+
* Once set, all SELECTs automatically add `WHERE "col" IS NULL`.
|
|
1963
|
+
* Use `.withDeleted()` on the query to opt out.
|
|
1964
|
+
*
|
|
1965
|
+
* The column must exist in the schema (validated in `build()`).
|
|
1966
|
+
*
|
|
1967
|
+
* @example
|
|
1968
|
+
* const usersTable = defineTable('users', {
|
|
1969
|
+
* id: column.integer().primaryKey(),
|
|
1970
|
+
* deletedAt: column.timestamp().nullable(),
|
|
1971
|
+
* }).withSoftDelete('deletedAt').build()
|
|
1972
|
+
*/
|
|
1973
|
+
withSoftDelete(col) {
|
|
1974
|
+
this._softDeleteColumn = col;
|
|
1975
|
+
return this;
|
|
1976
|
+
}
|
|
1297
1977
|
emits(map) {
|
|
1298
1978
|
const next = new _TableBuilder(this._name, this._schema);
|
|
1299
1979
|
for (const h of this._hooks) next._hooks.push(h);
|
|
1300
1980
|
next._events = map;
|
|
1981
|
+
for (const [k, v] of Object.entries(this._relations)) {
|
|
1982
|
+
;
|
|
1983
|
+
next._relations[k] = v;
|
|
1984
|
+
}
|
|
1985
|
+
;
|
|
1986
|
+
next._softDeleteColumn = this._softDeleteColumn;
|
|
1301
1987
|
return next;
|
|
1302
1988
|
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Declare a belongs-to relation — FK lives on this table.
|
|
1991
|
+
* Returns a new builder with the relation type added to TRelations.
|
|
1992
|
+
*
|
|
1993
|
+
* @example
|
|
1994
|
+
* const postsTable = defineTable('posts', { authorId: column.integer() })
|
|
1995
|
+
* .belongsTo('author', () => usersTable, 'authorId')
|
|
1996
|
+
* .build()
|
|
1997
|
+
*/
|
|
1998
|
+
belongsTo(name, getTable, foreignKey) {
|
|
1999
|
+
if (name in this._relations) {
|
|
2000
|
+
throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
|
|
2001
|
+
}
|
|
2002
|
+
const rel = { kind: "belongsTo", name, getTable, foreignKey };
|
|
2003
|
+
this._relations[name] = rel;
|
|
2004
|
+
return this;
|
|
2005
|
+
}
|
|
2006
|
+
/**
|
|
2007
|
+
* Declare a has-many relation — FK lives on the foreign table.
|
|
2008
|
+
* Returns a new builder with the relation type added to TRelations.
|
|
2009
|
+
*
|
|
2010
|
+
* @example
|
|
2011
|
+
* const usersTable = defineTable('users', { id: column.integer().primaryKey() })
|
|
2012
|
+
* .hasMany('posts', () => postsTable, 'authorId')
|
|
2013
|
+
* .build()
|
|
2014
|
+
*/
|
|
2015
|
+
hasMany(name, getTable, foreignKey) {
|
|
2016
|
+
if (name in this._relations) {
|
|
2017
|
+
throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
|
|
2018
|
+
}
|
|
2019
|
+
const rel = { kind: "hasMany", name, getTable, foreignKey };
|
|
2020
|
+
this._relations[name] = rel;
|
|
2021
|
+
return this;
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Declare a many-to-many relation via a pivot table.
|
|
2025
|
+
*
|
|
2026
|
+
* @example
|
|
2027
|
+
* const postsTable = defineTable('posts', { ... })
|
|
2028
|
+
* .manyToMany('tags', () => tagsTable, postTagsTable, 'postId', 'tagId')
|
|
2029
|
+
* .build()
|
|
2030
|
+
*/
|
|
2031
|
+
manyToMany(name, getTable, pivotTable, localKey, foreignKey) {
|
|
2032
|
+
if (name in this._relations) {
|
|
2033
|
+
throw new Error(`Relation '${name}' is already defined on table '${this._name}'`);
|
|
2034
|
+
}
|
|
2035
|
+
this._relations[name] = {
|
|
2036
|
+
kind: "manyToMany",
|
|
2037
|
+
name,
|
|
2038
|
+
getTable,
|
|
2039
|
+
foreignKey,
|
|
2040
|
+
pivot: { table: pivotTable, localKey, foreignKey }
|
|
2041
|
+
};
|
|
2042
|
+
return this;
|
|
2043
|
+
}
|
|
1303
2044
|
build() {
|
|
2045
|
+
if (this._softDeleteColumn !== null && !(this._softDeleteColumn in this._schema)) {
|
|
2046
|
+
throw new Error(
|
|
2047
|
+
`withSoftDelete: column '${this._softDeleteColumn}' is not defined in table '${this._name}'. Add it to the schema: column.timestamp().nullable()`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
1304
2050
|
return {
|
|
1305
2051
|
name: this._name,
|
|
1306
2052
|
schema: this._schema,
|
|
@@ -1312,7 +2058,9 @@ var TableBuilder = class _TableBuilder {
|
|
|
1312
2058
|
// At runtime it's an empty object (events hold only the string names, not payloads).
|
|
1313
2059
|
// The field exists solely so TypeScript can infer TMap in onEvent() without
|
|
1314
2060
|
// recomputing the conditional InferTableEvents each time.
|
|
1315
|
-
_eventMap: {}
|
|
2061
|
+
_eventMap: {},
|
|
2062
|
+
relations: { ...this._relations },
|
|
2063
|
+
softDeleteColumn: this._softDeleteColumn
|
|
1316
2064
|
};
|
|
1317
2065
|
}
|
|
1318
2066
|
_findPrimaryKey() {
|
|
@@ -1390,6 +2138,105 @@ function applyRedact(row, fields) {
|
|
|
1390
2138
|
return copy;
|
|
1391
2139
|
}
|
|
1392
2140
|
|
|
2141
|
+
// src/hooks/executor.ts
|
|
2142
|
+
var HookExecutor = class {
|
|
2143
|
+
// tableName → ordered array of module-level handlers
|
|
2144
|
+
registry = /* @__PURE__ */ new Map();
|
|
2145
|
+
// Set by dbPlugin at install() — used by app.register() to build audit closures.
|
|
2146
|
+
// undefined until dbPlugin installs (no-DB apps never set this).
|
|
2147
|
+
_adapter;
|
|
2148
|
+
// No EventBus held globally — queue is passed per runAfterX call
|
|
2149
|
+
constructor() {
|
|
2150
|
+
}
|
|
2151
|
+
setAdapter(adapter) {
|
|
2152
|
+
this._adapter = adapter;
|
|
2153
|
+
}
|
|
2154
|
+
getAdapter() {
|
|
2155
|
+
return this._adapter;
|
|
2156
|
+
}
|
|
2157
|
+
// Called by defineModule when a .hook() is registered
|
|
2158
|
+
registerModuleHook(tableName, handlers) {
|
|
2159
|
+
if (!this.registry.has(tableName)) this.registry.set(tableName, []);
|
|
2160
|
+
this.registry.get(tableName).push(handlers);
|
|
2161
|
+
}
|
|
2162
|
+
// ── Before operations — can transform data, can throw to cancel ──
|
|
2163
|
+
async runBeforeInsert(table, ctx, data) {
|
|
2164
|
+
let current = { ...data };
|
|
2165
|
+
for (const h of table.hooks) {
|
|
2166
|
+
if (!h.beforeInsert) continue;
|
|
2167
|
+
const result = await h.beforeInsert(current);
|
|
2168
|
+
if (result != null) current = result;
|
|
2169
|
+
}
|
|
2170
|
+
for (const h of this._moduleHandlers(table.name)) {
|
|
2171
|
+
if (!h.beforeInsert) continue;
|
|
2172
|
+
const result = await h.beforeInsert(ctx, current);
|
|
2173
|
+
if (result != null) current = result;
|
|
2174
|
+
}
|
|
2175
|
+
return current;
|
|
2176
|
+
}
|
|
2177
|
+
async runBeforeUpdate(table, ctx, current, patch) {
|
|
2178
|
+
let currentPatch = { ...patch };
|
|
2179
|
+
for (const h of table.hooks) {
|
|
2180
|
+
if (!h.beforeUpdate) continue;
|
|
2181
|
+
const result = await h.beforeUpdate(current, currentPatch);
|
|
2182
|
+
if (result != null) currentPatch = result;
|
|
2183
|
+
}
|
|
2184
|
+
for (const h of this._moduleHandlers(table.name)) {
|
|
2185
|
+
if (!h.beforeUpdate) continue;
|
|
2186
|
+
const result = await h.beforeUpdate(ctx, current, currentPatch);
|
|
2187
|
+
if (result != null) currentPatch = result;
|
|
2188
|
+
}
|
|
2189
|
+
return currentPatch;
|
|
2190
|
+
}
|
|
2191
|
+
async runBeforeDelete(table, ctx, current) {
|
|
2192
|
+
for (const h of table.hooks) {
|
|
2193
|
+
if (h.beforeDelete) await h.beforeDelete(current);
|
|
2194
|
+
}
|
|
2195
|
+
for (const h of this._moduleHandlers(table.name)) {
|
|
2196
|
+
if (h.beforeDelete) await h.beforeDelete(ctx, current);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
// ── After operations — side effects only, cannot cancel ──
|
|
2200
|
+
// queue: per-request RequestEventQueue, or undefined when called outside HTTP context.
|
|
2201
|
+
// When undefined (e.g. background jobs, tests, Phase 2 direct usage), events are dropped.
|
|
2202
|
+
async runAfterInsert(table, ctx, result, input, queue) {
|
|
2203
|
+
for (const h of table.hooks) {
|
|
2204
|
+
if (h.afterInsert) await h.afterInsert(result, input);
|
|
2205
|
+
}
|
|
2206
|
+
for (const h of this._moduleHandlers(table.name)) {
|
|
2207
|
+
if (h.afterInsert) await h.afterInsert(ctx, result, input);
|
|
2208
|
+
}
|
|
2209
|
+
if (table.events.afterInsert) {
|
|
2210
|
+
queue?.collect(table.events.afterInsert, result);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
async runAfterUpdate(table, ctx, result, before, queue) {
|
|
2214
|
+
for (const h of table.hooks) {
|
|
2215
|
+
if (h.afterUpdate) await h.afterUpdate(result, before);
|
|
2216
|
+
}
|
|
2217
|
+
for (const h of this._moduleHandlers(table.name)) {
|
|
2218
|
+
if (h.afterUpdate) await h.afterUpdate(ctx, result, before);
|
|
2219
|
+
}
|
|
2220
|
+
if (table.events.afterUpdate) {
|
|
2221
|
+
queue?.collect(table.events.afterUpdate, { before, after: result });
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
async runAfterDelete(table, ctx, deleted, queue) {
|
|
2225
|
+
for (const h of table.hooks) {
|
|
2226
|
+
if (h.afterDelete) await h.afterDelete(deleted);
|
|
2227
|
+
}
|
|
2228
|
+
for (const h of this._moduleHandlers(table.name)) {
|
|
2229
|
+
if (h.afterDelete) await h.afterDelete(ctx, deleted);
|
|
2230
|
+
}
|
|
2231
|
+
if (table.events.afterDelete) {
|
|
2232
|
+
queue?.collect(table.events.afterDelete, deleted);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
_moduleHandlers(tableName) {
|
|
2236
|
+
return this.registry.get(tableName) ?? [];
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
|
|
1393
2240
|
// src/db/migrations/runner.ts
|
|
1394
2241
|
import { readdir, readFile } from "fs/promises";
|
|
1395
2242
|
import { join } from "path";
|
|
@@ -1977,6 +2824,7 @@ async function generateMigration(options) {
|
|
|
1977
2824
|
|
|
1978
2825
|
// src/index.ts
|
|
1979
2826
|
init_db();
|
|
2827
|
+
init_sql();
|
|
1980
2828
|
init_events();
|
|
1981
2829
|
|
|
1982
2830
|
// src/events/handler.ts
|
|
@@ -2671,105 +3519,6 @@ function createSystemCtx(extra) {
|
|
|
2671
3519
|
init_db();
|
|
2672
3520
|
init_events();
|
|
2673
3521
|
|
|
2674
|
-
// src/hooks/executor.ts
|
|
2675
|
-
var HookExecutor = class {
|
|
2676
|
-
// tableName → ordered array of module-level handlers
|
|
2677
|
-
registry = /* @__PURE__ */ new Map();
|
|
2678
|
-
// Set by dbPlugin at install() — used by app.register() to build audit closures.
|
|
2679
|
-
// undefined until dbPlugin installs (no-DB apps never set this).
|
|
2680
|
-
_adapter;
|
|
2681
|
-
// No EventBus held globally — queue is passed per runAfterX call
|
|
2682
|
-
constructor() {
|
|
2683
|
-
}
|
|
2684
|
-
setAdapter(adapter) {
|
|
2685
|
-
this._adapter = adapter;
|
|
2686
|
-
}
|
|
2687
|
-
getAdapter() {
|
|
2688
|
-
return this._adapter;
|
|
2689
|
-
}
|
|
2690
|
-
// Called by defineModule when a .hook() is registered
|
|
2691
|
-
registerModuleHook(tableName, handlers) {
|
|
2692
|
-
if (!this.registry.has(tableName)) this.registry.set(tableName, []);
|
|
2693
|
-
this.registry.get(tableName).push(handlers);
|
|
2694
|
-
}
|
|
2695
|
-
// ── Before operations — can transform data, can throw to cancel ──
|
|
2696
|
-
async runBeforeInsert(table, ctx, data) {
|
|
2697
|
-
let current = { ...data };
|
|
2698
|
-
for (const h of table.hooks) {
|
|
2699
|
-
if (!h.beforeInsert) continue;
|
|
2700
|
-
const result = await h.beforeInsert(current);
|
|
2701
|
-
if (result != null) current = result;
|
|
2702
|
-
}
|
|
2703
|
-
for (const h of this._moduleHandlers(table.name)) {
|
|
2704
|
-
if (!h.beforeInsert) continue;
|
|
2705
|
-
const result = await h.beforeInsert(ctx, current);
|
|
2706
|
-
if (result != null) current = result;
|
|
2707
|
-
}
|
|
2708
|
-
return current;
|
|
2709
|
-
}
|
|
2710
|
-
async runBeforeUpdate(table, ctx, current, patch) {
|
|
2711
|
-
let currentPatch = { ...patch };
|
|
2712
|
-
for (const h of table.hooks) {
|
|
2713
|
-
if (!h.beforeUpdate) continue;
|
|
2714
|
-
const result = await h.beforeUpdate(current, currentPatch);
|
|
2715
|
-
if (result != null) currentPatch = result;
|
|
2716
|
-
}
|
|
2717
|
-
for (const h of this._moduleHandlers(table.name)) {
|
|
2718
|
-
if (!h.beforeUpdate) continue;
|
|
2719
|
-
const result = await h.beforeUpdate(ctx, current, currentPatch);
|
|
2720
|
-
if (result != null) currentPatch = result;
|
|
2721
|
-
}
|
|
2722
|
-
return currentPatch;
|
|
2723
|
-
}
|
|
2724
|
-
async runBeforeDelete(table, ctx, current) {
|
|
2725
|
-
for (const h of table.hooks) {
|
|
2726
|
-
if (h.beforeDelete) await h.beforeDelete(current);
|
|
2727
|
-
}
|
|
2728
|
-
for (const h of this._moduleHandlers(table.name)) {
|
|
2729
|
-
if (h.beforeDelete) await h.beforeDelete(ctx, current);
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
// ── After operations — side effects only, cannot cancel ──
|
|
2733
|
-
// queue: per-request RequestEventQueue, or undefined when called outside HTTP context.
|
|
2734
|
-
// When undefined (e.g. background jobs, tests, Phase 2 direct usage), events are dropped.
|
|
2735
|
-
async runAfterInsert(table, ctx, result, input, queue) {
|
|
2736
|
-
for (const h of table.hooks) {
|
|
2737
|
-
if (h.afterInsert) await h.afterInsert(result, input);
|
|
2738
|
-
}
|
|
2739
|
-
for (const h of this._moduleHandlers(table.name)) {
|
|
2740
|
-
if (h.afterInsert) await h.afterInsert(ctx, result, input);
|
|
2741
|
-
}
|
|
2742
|
-
if (table.events.afterInsert) {
|
|
2743
|
-
queue?.collect(table.events.afterInsert, result);
|
|
2744
|
-
}
|
|
2745
|
-
}
|
|
2746
|
-
async runAfterUpdate(table, ctx, result, before, queue) {
|
|
2747
|
-
for (const h of table.hooks) {
|
|
2748
|
-
if (h.afterUpdate) await h.afterUpdate(result, before);
|
|
2749
|
-
}
|
|
2750
|
-
for (const h of this._moduleHandlers(table.name)) {
|
|
2751
|
-
if (h.afterUpdate) await h.afterUpdate(ctx, result, before);
|
|
2752
|
-
}
|
|
2753
|
-
if (table.events.afterUpdate) {
|
|
2754
|
-
queue?.collect(table.events.afterUpdate, { before, after: result });
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
async runAfterDelete(table, ctx, deleted, queue) {
|
|
2758
|
-
for (const h of table.hooks) {
|
|
2759
|
-
if (h.afterDelete) await h.afterDelete(deleted);
|
|
2760
|
-
}
|
|
2761
|
-
for (const h of this._moduleHandlers(table.name)) {
|
|
2762
|
-
if (h.afterDelete) await h.afterDelete(ctx, deleted);
|
|
2763
|
-
}
|
|
2764
|
-
if (table.events.afterDelete) {
|
|
2765
|
-
queue?.collect(table.events.afterDelete, deleted);
|
|
2766
|
-
}
|
|
2767
|
-
}
|
|
2768
|
-
_moduleHandlers(tableName) {
|
|
2769
|
-
return this.registry.get(tableName) ?? [];
|
|
2770
|
-
}
|
|
2771
|
-
};
|
|
2772
|
-
|
|
2773
3522
|
// src/app/router.ts
|
|
2774
3523
|
function matchPath(pattern, pathname) {
|
|
2775
3524
|
const normPattern = pattern.length > 1 ? pattern.replace(/\/$/, "") : pattern;
|
|
@@ -5500,6 +6249,7 @@ export {
|
|
|
5500
6249
|
EventBus,
|
|
5501
6250
|
EventHandlerBuilder,
|
|
5502
6251
|
ForbiddenError,
|
|
6252
|
+
HookExecutor,
|
|
5503
6253
|
InMemoryEventBus,
|
|
5504
6254
|
InMemoryStore,
|
|
5505
6255
|
InsertBuilder,
|
|
@@ -5517,8 +6267,10 @@ export {
|
|
|
5517
6267
|
ResourceBuilder,
|
|
5518
6268
|
SQLiteAdapter,
|
|
5519
6269
|
SelectBuilder,
|
|
6270
|
+
SoftDeleteBuilder,
|
|
5520
6271
|
TooManyRequestsError,
|
|
5521
6272
|
UnauthorizedError,
|
|
6273
|
+
UnionBuilder,
|
|
5522
6274
|
UnprocessableError,
|
|
5523
6275
|
ValidationError,
|
|
5524
6276
|
Veln,
|
|
@@ -5526,6 +6278,8 @@ export {
|
|
|
5526
6278
|
VelnDB,
|
|
5527
6279
|
VelnError,
|
|
5528
6280
|
bodySizeLimitPlugin,
|
|
6281
|
+
buildSubquery,
|
|
6282
|
+
buildUnion,
|
|
5529
6283
|
column,
|
|
5530
6284
|
compareSchemas,
|
|
5531
6285
|
compressionPlugin,
|