@vertz/db 0.2.15 → 0.2.17
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/d1/index.d.ts +145 -11
- package/dist/d1/index.js +1 -1
- package/dist/index.d.ts +263 -209
- package/dist/index.js +244 -147
- package/dist/postgres/index.d.ts +6 -0
- package/dist/postgres/index.js +1 -1
- package/dist/shared/{chunk-pxjcpnpx.js → chunk-3a9nybt2.js} +7 -2
- package/dist/shared/{chunk-pkv8w501.js → chunk-ed82s3sa.js} +8 -4
- package/dist/shared/{chunk-2gd1fqcw.js → chunk-p2x2vmg5.js} +1 -1
- package/dist/shared/{chunk-rqe0prft.js → chunk-rjry8vc2.js} +23 -0
- package/dist/shared/{chunk-ndxe1h28.js → chunk-tnaf4hbj.js} +1 -1
- package/dist/sql/index.js +1 -1
- package/dist/sqlite/index.d.ts +145 -11
- package/dist/sqlite/index.js +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
buildSelect,
|
|
5
5
|
buildUpdate,
|
|
6
6
|
buildWhere
|
|
7
|
-
} from "./shared/chunk-
|
|
7
|
+
} from "./shared/chunk-3a9nybt2.js";
|
|
8
8
|
import {
|
|
9
9
|
CheckConstraintError,
|
|
10
10
|
ConnectionError,
|
|
@@ -47,33 +47,31 @@ import {
|
|
|
47
47
|
import {
|
|
48
48
|
createD1Adapter,
|
|
49
49
|
createD1Driver
|
|
50
|
-
} from "./shared/chunk-
|
|
50
|
+
} from "./shared/chunk-tnaf4hbj.js";
|
|
51
51
|
import {
|
|
52
52
|
generateId
|
|
53
|
-
} from "./shared/chunk-
|
|
53
|
+
} from "./shared/chunk-ed82s3sa.js";
|
|
54
54
|
// src/adapters/database-bridge-adapter.ts
|
|
55
55
|
function createDatabaseBridgeAdapter(db, tableName) {
|
|
56
56
|
const delegate = db[tableName];
|
|
57
57
|
return {
|
|
58
|
-
async get(id) {
|
|
59
|
-
const result = await delegate.get({
|
|
58
|
+
async get(id, options) {
|
|
59
|
+
const result = await delegate.get({
|
|
60
|
+
where: { id },
|
|
61
|
+
...options?.include && { include: options.include }
|
|
62
|
+
});
|
|
60
63
|
if (!result.ok) {
|
|
61
64
|
return null;
|
|
62
65
|
}
|
|
63
66
|
return result.data;
|
|
64
67
|
},
|
|
65
68
|
async list(options) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
if (options?.limit !== undefined) {
|
|
74
|
-
dbOptions.limit = options.limit;
|
|
75
|
-
}
|
|
76
|
-
const result = await delegate.listAndCount(dbOptions);
|
|
69
|
+
const result = await delegate.listAndCount({
|
|
70
|
+
...options?.where && { where: options.where },
|
|
71
|
+
...options?.orderBy && { orderBy: options.orderBy },
|
|
72
|
+
...options?.limit !== undefined && { limit: options.limit },
|
|
73
|
+
...options?.include && { include: options.include }
|
|
74
|
+
});
|
|
77
75
|
if (!result.ok) {
|
|
78
76
|
throw result.error;
|
|
79
77
|
}
|
|
@@ -1614,10 +1612,32 @@ function resolvePkColumn(table) {
|
|
|
1614
1612
|
const first = pkCols[0];
|
|
1615
1613
|
return first !== undefined ? first : "id";
|
|
1616
1614
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1615
|
+
var MAX_RELATION_QUERY_BUDGET = 50;
|
|
1616
|
+
var DEFAULT_RELATION_LIMIT = 100;
|
|
1617
|
+
var GLOBAL_RELATION_ROW_LIMIT = 1e4;
|
|
1618
|
+
function safeMergeWhere(batchWhere, userWhere) {
|
|
1619
|
+
if (!userWhere)
|
|
1620
|
+
return batchWhere;
|
|
1621
|
+
const merged = { ...batchWhere };
|
|
1622
|
+
for (const [key, value] of Object.entries(userWhere)) {
|
|
1623
|
+
if (!(key in batchWhere)) {
|
|
1624
|
+
merged[key] = value;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return merged;
|
|
1628
|
+
}
|
|
1629
|
+
function resolveEffectiveLimit(includeValue) {
|
|
1630
|
+
const userLimit = typeof includeValue === "object" ? includeValue.limit : undefined;
|
|
1631
|
+
if (typeof userLimit === "number" && userLimit > 0) {
|
|
1632
|
+
return userLimit;
|
|
1633
|
+
}
|
|
1634
|
+
return DEFAULT_RELATION_LIMIT;
|
|
1635
|
+
}
|
|
1636
|
+
async function loadRelations(queryFn, primaryRows, relations, include, depth = 0, tablesRegistry, primaryTable, queryBudget) {
|
|
1637
|
+
if (depth > 3 || primaryRows.length === 0) {
|
|
1619
1638
|
return primaryRows;
|
|
1620
1639
|
}
|
|
1640
|
+
const budget = queryBudget ?? { remaining: MAX_RELATION_QUERY_BUDGET };
|
|
1621
1641
|
const toLoad = [];
|
|
1622
1642
|
for (const [key, value] of Object.entries(include)) {
|
|
1623
1643
|
if (value === undefined)
|
|
@@ -1633,11 +1653,11 @@ async function loadRelations(queryFn, primaryRows, relations, include, depth = 0
|
|
|
1633
1653
|
for (const { key: relName, def, includeValue } of toLoad) {
|
|
1634
1654
|
const target = def._target();
|
|
1635
1655
|
if (def._type === "one") {
|
|
1636
|
-
await loadOneRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry);
|
|
1656
|
+
await loadOneRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, budget);
|
|
1637
1657
|
} else if (def._through) {
|
|
1638
|
-
await loadManyToManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable);
|
|
1658
|
+
await loadManyToManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable, budget);
|
|
1639
1659
|
} else {
|
|
1640
|
-
await loadManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable);
|
|
1660
|
+
await loadManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable, budget);
|
|
1641
1661
|
}
|
|
1642
1662
|
}
|
|
1643
1663
|
return primaryRows;
|
|
@@ -1652,7 +1672,7 @@ function findTargetRelations(target, tablesRegistry) {
|
|
|
1652
1672
|
}
|
|
1653
1673
|
return;
|
|
1654
1674
|
}
|
|
1655
|
-
async function loadOneRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry) {
|
|
1675
|
+
async function loadOneRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, queryBudget) {
|
|
1656
1676
|
const fk = def._foreignKey;
|
|
1657
1677
|
if (!fk)
|
|
1658
1678
|
return;
|
|
@@ -1675,22 +1695,29 @@ async function loadOneRelation(queryFn, primaryRows, def, target, relName, inclu
|
|
|
1675
1695
|
if (!columns.includes(targetPk)) {
|
|
1676
1696
|
columns.push(targetPk);
|
|
1677
1697
|
}
|
|
1698
|
+
if (queryBudget && queryBudget.remaining <= 0) {
|
|
1699
|
+
throw new Error(`Relation query budget exceeded (max ${MAX_RELATION_QUERY_BUDGET} queries). ` + "Reduce include depth or number of included relations.");
|
|
1700
|
+
}
|
|
1701
|
+
const userWhere = typeof includeValue === "object" ? includeValue.where : undefined;
|
|
1702
|
+
const batchWhere = safeMergeWhere({ [targetPk]: { in: [...fkValues] } }, userWhere);
|
|
1678
1703
|
const query = buildSelect({
|
|
1679
1704
|
table: target._name,
|
|
1680
1705
|
columns,
|
|
1681
|
-
where:
|
|
1706
|
+
where: batchWhere
|
|
1682
1707
|
});
|
|
1708
|
+
if (queryBudget)
|
|
1709
|
+
queryBudget.remaining--;
|
|
1683
1710
|
const res = await executeQuery(queryFn, query.sql, query.params);
|
|
1684
1711
|
const lookup = new Map;
|
|
1685
1712
|
for (const row of res.rows) {
|
|
1686
1713
|
const mapped = mapRow(row);
|
|
1687
1714
|
lookup.set(mapped[targetPk], mapped);
|
|
1688
1715
|
}
|
|
1689
|
-
if (typeof includeValue === "object" && includeValue.include && depth <
|
|
1716
|
+
if (typeof includeValue === "object" && includeValue.include && depth < 3) {
|
|
1690
1717
|
const targetRelations = findTargetRelations(target, tablesRegistry);
|
|
1691
1718
|
if (targetRelations) {
|
|
1692
1719
|
const childRows = [...lookup.values()];
|
|
1693
|
-
await loadRelations(queryFn, childRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target);
|
|
1720
|
+
await loadRelations(queryFn, childRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target, queryBudget);
|
|
1694
1721
|
}
|
|
1695
1722
|
}
|
|
1696
1723
|
for (const row of primaryRows) {
|
|
@@ -1698,7 +1725,7 @@ async function loadOneRelation(queryFn, primaryRows, def, target, relName, inclu
|
|
|
1698
1725
|
row[relName] = lookup.get(fkVal) ?? null;
|
|
1699
1726
|
}
|
|
1700
1727
|
}
|
|
1701
|
-
async function loadManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable) {
|
|
1728
|
+
async function loadManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable, queryBudget) {
|
|
1702
1729
|
const fk = def._foreignKey;
|
|
1703
1730
|
if (!fk)
|
|
1704
1731
|
return;
|
|
@@ -1721,11 +1748,21 @@ async function loadManyRelation(queryFn, primaryRows, def, target, relName, incl
|
|
|
1721
1748
|
if (!columns.includes(fk)) {
|
|
1722
1749
|
columns.push(fk);
|
|
1723
1750
|
}
|
|
1751
|
+
const userWhere = typeof includeValue === "object" ? includeValue.where : undefined;
|
|
1752
|
+
const batchWhere = safeMergeWhere({ [fk]: { in: [...pkValues] } }, userWhere);
|
|
1753
|
+
const userOrderBy = typeof includeValue === "object" ? includeValue.orderBy : undefined;
|
|
1754
|
+
if (queryBudget && queryBudget.remaining <= 0) {
|
|
1755
|
+
throw new Error(`Relation query budget exceeded (max ${MAX_RELATION_QUERY_BUDGET} queries). ` + "Reduce include depth or number of included relations.");
|
|
1756
|
+
}
|
|
1724
1757
|
const query = buildSelect({
|
|
1725
1758
|
table: target._name,
|
|
1726
1759
|
columns,
|
|
1727
|
-
where:
|
|
1760
|
+
where: batchWhere,
|
|
1761
|
+
orderBy: userOrderBy,
|
|
1762
|
+
limit: GLOBAL_RELATION_ROW_LIMIT
|
|
1728
1763
|
});
|
|
1764
|
+
if (queryBudget)
|
|
1765
|
+
queryBudget.remaining--;
|
|
1729
1766
|
const res = await executeQuery(queryFn, query.sql, query.params);
|
|
1730
1767
|
const lookup = new Map;
|
|
1731
1768
|
for (const row of res.rows) {
|
|
@@ -1738,11 +1775,17 @@ async function loadManyRelation(queryFn, primaryRows, def, target, relName, incl
|
|
|
1738
1775
|
lookup.set(parentId, [mapped]);
|
|
1739
1776
|
}
|
|
1740
1777
|
}
|
|
1741
|
-
|
|
1778
|
+
const effectiveLimit = resolveEffectiveLimit(includeValue);
|
|
1779
|
+
for (const [parentId, rows] of lookup) {
|
|
1780
|
+
if (rows.length > effectiveLimit) {
|
|
1781
|
+
lookup.set(parentId, rows.slice(0, effectiveLimit));
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (typeof includeValue === "object" && includeValue.include && depth < 3) {
|
|
1742
1785
|
const targetRelations = findTargetRelations(target, tablesRegistry);
|
|
1743
1786
|
if (targetRelations) {
|
|
1744
1787
|
const allChildRows = [...lookup.values()].flat();
|
|
1745
|
-
await loadRelations(queryFn, allChildRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target);
|
|
1788
|
+
await loadRelations(queryFn, allChildRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target, queryBudget);
|
|
1746
1789
|
}
|
|
1747
1790
|
}
|
|
1748
1791
|
for (const row of primaryRows) {
|
|
@@ -1750,7 +1793,7 @@ async function loadManyRelation(queryFn, primaryRows, def, target, relName, incl
|
|
|
1750
1793
|
row[relName] = lookup.get(pkVal) ?? [];
|
|
1751
1794
|
}
|
|
1752
1795
|
}
|
|
1753
|
-
async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable) {
|
|
1796
|
+
async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName, includeValue, depth, tablesRegistry, primaryTable, queryBudget) {
|
|
1754
1797
|
const through = def._through;
|
|
1755
1798
|
if (!through)
|
|
1756
1799
|
return;
|
|
@@ -1772,11 +1815,16 @@ async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName
|
|
|
1772
1815
|
}
|
|
1773
1816
|
return;
|
|
1774
1817
|
}
|
|
1818
|
+
if (queryBudget && queryBudget.remaining <= 0) {
|
|
1819
|
+
throw new Error(`Relation query budget exceeded (max ${MAX_RELATION_QUERY_BUDGET} queries). ` + "Reduce include depth or number of included relations.");
|
|
1820
|
+
}
|
|
1775
1821
|
const joinQuery = buildSelect({
|
|
1776
1822
|
table: joinTable._name,
|
|
1777
1823
|
columns: [thisKey, thatKey],
|
|
1778
1824
|
where: { [thisKey]: { in: [...pkValues] } }
|
|
1779
1825
|
});
|
|
1826
|
+
if (queryBudget)
|
|
1827
|
+
queryBudget.remaining--;
|
|
1780
1828
|
const joinRes = await executeQuery(queryFn, joinQuery.sql, joinQuery.params);
|
|
1781
1829
|
const primaryToTargetIds = new Map;
|
|
1782
1830
|
const allTargetIds = new Set;
|
|
@@ -1805,22 +1853,32 @@ async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName
|
|
|
1805
1853
|
if (!columns.includes(targetPk)) {
|
|
1806
1854
|
columns.push(targetPk);
|
|
1807
1855
|
}
|
|
1856
|
+
const userWhere = typeof includeValue === "object" ? includeValue.where : undefined;
|
|
1857
|
+
const targetWhere = safeMergeWhere({ [targetPk]: { in: [...allTargetIds] } }, userWhere);
|
|
1858
|
+
const userOrderBy = typeof includeValue === "object" ? includeValue.orderBy : undefined;
|
|
1859
|
+
if (queryBudget && queryBudget.remaining <= 0) {
|
|
1860
|
+
throw new Error(`Relation query budget exceeded (max ${MAX_RELATION_QUERY_BUDGET} queries). ` + "Reduce include depth or number of included relations.");
|
|
1861
|
+
}
|
|
1808
1862
|
const targetQuery = buildSelect({
|
|
1809
1863
|
table: target._name,
|
|
1810
1864
|
columns,
|
|
1811
|
-
where:
|
|
1865
|
+
where: targetWhere,
|
|
1866
|
+
orderBy: userOrderBy,
|
|
1867
|
+
limit: GLOBAL_RELATION_ROW_LIMIT
|
|
1812
1868
|
});
|
|
1869
|
+
if (queryBudget)
|
|
1870
|
+
queryBudget.remaining--;
|
|
1813
1871
|
const targetRes = await executeQuery(queryFn, targetQuery.sql, targetQuery.params);
|
|
1814
1872
|
const targetLookup = new Map;
|
|
1815
1873
|
for (const row of targetRes.rows) {
|
|
1816
1874
|
const mapped = mapRow(row);
|
|
1817
1875
|
targetLookup.set(mapped[targetPk], mapped);
|
|
1818
1876
|
}
|
|
1819
|
-
if (typeof includeValue === "object" && includeValue.include && depth <
|
|
1877
|
+
if (typeof includeValue === "object" && includeValue.include && depth < 3) {
|
|
1820
1878
|
const targetRelations = findTargetRelations(target, tablesRegistry);
|
|
1821
1879
|
if (targetRelations) {
|
|
1822
1880
|
const allTargetRows = [...targetLookup.values()];
|
|
1823
|
-
await loadRelations(queryFn, allTargetRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target);
|
|
1881
|
+
await loadRelations(queryFn, allTargetRows, targetRelations, includeValue.include, depth + 1, tablesRegistry, target, queryBudget);
|
|
1824
1882
|
}
|
|
1825
1883
|
}
|
|
1826
1884
|
for (const row of primaryRows) {
|
|
@@ -2058,104 +2116,15 @@ function resolveModel(models, name) {
|
|
|
2058
2116
|
}
|
|
2059
2117
|
return entry;
|
|
2060
2118
|
}
|
|
2061
|
-
var RESERVED_MODEL_NAMES = new Set(["query", "close", "isHealthy", "_internals"]);
|
|
2062
|
-
function
|
|
2063
|
-
const { models, log, dialect } = options;
|
|
2064
|
-
for (const key of Object.keys(models)) {
|
|
2065
|
-
if (RESERVED_MODEL_NAMES.has(key)) {
|
|
2066
|
-
throw new Error(`Model name "${key}" is reserved. Choose a different name for this model. ` + `Reserved names: ${[...RESERVED_MODEL_NAMES].join(", ")}`);
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
if (dialect === "sqlite") {
|
|
2070
|
-
if (!options.d1) {
|
|
2071
|
-
throw new Error("SQLite dialect requires a D1 binding");
|
|
2072
|
-
}
|
|
2073
|
-
if (options.url) {
|
|
2074
|
-
throw new Error("SQLite dialect uses D1, not a connection URL");
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
const dialectObj = dialect === "sqlite" ? defaultSqliteDialect : defaultPostgresDialect;
|
|
2078
|
-
const tenantGraph = computeTenantGraph(models);
|
|
2079
|
-
if (log && tenantGraph.root !== null) {
|
|
2080
|
-
const allScoped = new Set([
|
|
2081
|
-
...tenantGraph.root !== null ? [tenantGraph.root] : [],
|
|
2082
|
-
...tenantGraph.directlyScoped,
|
|
2083
|
-
...tenantGraph.indirectlyScoped,
|
|
2084
|
-
...tenantGraph.shared
|
|
2085
|
-
]);
|
|
2086
|
-
for (const [key, entry] of Object.entries(models)) {
|
|
2087
|
-
if (!allScoped.has(key)) {
|
|
2088
|
-
log(`[vertz/db] Table "${entry.table._name}" has no tenant path and is not marked .shared(). ` + "It will not be automatically scoped to a tenant.");
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
const modelsRegistry = models;
|
|
2093
|
-
let driver = null;
|
|
2094
|
-
let sqliteDriver = null;
|
|
2095
|
-
let replicaDrivers = [];
|
|
2096
|
-
let replicaIndex = 0;
|
|
2097
|
-
const queryFn = (() => {
|
|
2098
|
-
if (options._queryFn) {
|
|
2099
|
-
return options._queryFn;
|
|
2100
|
-
}
|
|
2101
|
-
if (dialect === "sqlite" && options.d1) {
|
|
2102
|
-
const tableSchema = buildTableSchema(models);
|
|
2103
|
-
sqliteDriver = createSqliteDriver(options.d1, tableSchema);
|
|
2104
|
-
return async (sqlStr, params) => {
|
|
2105
|
-
if (!sqliteDriver) {
|
|
2106
|
-
throw new Error("SQLite driver not initialized");
|
|
2107
|
-
}
|
|
2108
|
-
const rows = await sqliteDriver.query(sqlStr, params);
|
|
2109
|
-
return { rows, rowCount: rows.length };
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
if (options.url) {
|
|
2113
|
-
let initialized = false;
|
|
2114
|
-
const initPostgres = async () => {
|
|
2115
|
-
if (initialized)
|
|
2116
|
-
return;
|
|
2117
|
-
const { createPostgresDriver } = await import("./shared/chunk-2gd1fqcw.js");
|
|
2118
|
-
driver = createPostgresDriver(options.url, options.pool);
|
|
2119
|
-
const replicas = options.pool?.replicas;
|
|
2120
|
-
if (replicas && replicas.length > 0) {
|
|
2121
|
-
replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
|
|
2122
|
-
}
|
|
2123
|
-
initialized = true;
|
|
2124
|
-
};
|
|
2125
|
-
return async (sqlStr, params) => {
|
|
2126
|
-
await initPostgres();
|
|
2127
|
-
if (replicaDrivers.length === 0) {
|
|
2128
|
-
if (!driver) {
|
|
2129
|
-
throw new Error("Database driver not initialized");
|
|
2130
|
-
}
|
|
2131
|
-
return driver.queryFn(sqlStr, params);
|
|
2132
|
-
}
|
|
2133
|
-
if (isReadQuery(sqlStr)) {
|
|
2134
|
-
const targetReplica = replicaDrivers[replicaIndex];
|
|
2135
|
-
replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
|
|
2136
|
-
try {
|
|
2137
|
-
return await targetReplica.queryFn(sqlStr, params);
|
|
2138
|
-
} catch (err3) {
|
|
2139
|
-
console.warn("[vertz/db] replica query failed, falling back to primary:", err3.message);
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
if (!driver) {
|
|
2143
|
-
throw new Error("Database driver not initialized");
|
|
2144
|
-
}
|
|
2145
|
-
return driver.queryFn(sqlStr, params);
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
return async () => {
|
|
2149
|
-
throw new Error("db.query() requires a connected database driver. Provide a `url` to connect to PostgreSQL, a `dialect` with D1 binding for SQLite, or `_queryFn` for testing.");
|
|
2150
|
-
};
|
|
2151
|
-
})();
|
|
2119
|
+
var RESERVED_MODEL_NAMES = new Set(["query", "transaction", "close", "isHealthy", "_internals"]);
|
|
2120
|
+
function buildDelegates(qfn, models, dialectObj, modelsRegistry) {
|
|
2152
2121
|
function implGet(name, opts) {
|
|
2153
2122
|
return (async () => {
|
|
2154
2123
|
try {
|
|
2155
2124
|
const entry = resolveModel(models, name);
|
|
2156
|
-
const result = await get(
|
|
2125
|
+
const result = await get(qfn, entry.table, opts, dialectObj);
|
|
2157
2126
|
if (result !== null && opts?.include) {
|
|
2158
|
-
const rows = await loadRelations(
|
|
2127
|
+
const rows = await loadRelations(qfn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2159
2128
|
return ok(rows[0] ?? null);
|
|
2160
2129
|
}
|
|
2161
2130
|
return ok(result);
|
|
@@ -2168,7 +2137,7 @@ function createDb(options) {
|
|
|
2168
2137
|
return (async () => {
|
|
2169
2138
|
try {
|
|
2170
2139
|
const entry = resolveModel(models, name);
|
|
2171
|
-
const result = await get(
|
|
2140
|
+
const result = await get(qfn, entry.table, opts, dialectObj);
|
|
2172
2141
|
if (result === null) {
|
|
2173
2142
|
return err2({
|
|
2174
2143
|
code: "NotFound",
|
|
@@ -2177,7 +2146,7 @@ function createDb(options) {
|
|
|
2177
2146
|
});
|
|
2178
2147
|
}
|
|
2179
2148
|
if (opts?.include) {
|
|
2180
|
-
const rows = await loadRelations(
|
|
2149
|
+
const rows = await loadRelations(qfn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2181
2150
|
return ok(rows[0]);
|
|
2182
2151
|
}
|
|
2183
2152
|
return ok(result);
|
|
@@ -2190,9 +2159,9 @@ function createDb(options) {
|
|
|
2190
2159
|
return (async () => {
|
|
2191
2160
|
try {
|
|
2192
2161
|
const entry = resolveModel(models, name);
|
|
2193
|
-
const results = await list(
|
|
2162
|
+
const results = await list(qfn, entry.table, opts, dialectObj);
|
|
2194
2163
|
if (opts?.include && results.length > 0) {
|
|
2195
|
-
const withRelations = await loadRelations(
|
|
2164
|
+
const withRelations = await loadRelations(qfn, results, entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2196
2165
|
return ok(withRelations);
|
|
2197
2166
|
}
|
|
2198
2167
|
return ok(results);
|
|
@@ -2205,9 +2174,9 @@ function createDb(options) {
|
|
|
2205
2174
|
return (async () => {
|
|
2206
2175
|
try {
|
|
2207
2176
|
const entry = resolveModel(models, name);
|
|
2208
|
-
const { data, total } = await listAndCount(
|
|
2177
|
+
const { data, total } = await listAndCount(qfn, entry.table, opts, dialectObj);
|
|
2209
2178
|
if (opts?.include && data.length > 0) {
|
|
2210
|
-
const withRelations = await loadRelations(
|
|
2179
|
+
const withRelations = await loadRelations(qfn, data, entry.relations, opts.include, 0, modelsRegistry, entry.table);
|
|
2211
2180
|
return ok({ data: withRelations, total });
|
|
2212
2181
|
}
|
|
2213
2182
|
return ok({ data, total });
|
|
@@ -2220,7 +2189,7 @@ function createDb(options) {
|
|
|
2220
2189
|
return (async () => {
|
|
2221
2190
|
try {
|
|
2222
2191
|
const entry = resolveModel(models, name);
|
|
2223
|
-
const result = await create(
|
|
2192
|
+
const result = await create(qfn, entry.table, opts, dialectObj);
|
|
2224
2193
|
return ok(result);
|
|
2225
2194
|
} catch (e) {
|
|
2226
2195
|
return err2(toWriteError(e));
|
|
@@ -2231,7 +2200,7 @@ function createDb(options) {
|
|
|
2231
2200
|
return (async () => {
|
|
2232
2201
|
try {
|
|
2233
2202
|
const entry = resolveModel(models, name);
|
|
2234
|
-
const result = await createMany(
|
|
2203
|
+
const result = await createMany(qfn, entry.table, opts, dialectObj);
|
|
2235
2204
|
return ok(result);
|
|
2236
2205
|
} catch (e) {
|
|
2237
2206
|
return err2(toWriteError(e));
|
|
@@ -2242,7 +2211,7 @@ function createDb(options) {
|
|
|
2242
2211
|
return (async () => {
|
|
2243
2212
|
try {
|
|
2244
2213
|
const entry = resolveModel(models, name);
|
|
2245
|
-
const result = await createManyAndReturn(
|
|
2214
|
+
const result = await createManyAndReturn(qfn, entry.table, opts, dialectObj);
|
|
2246
2215
|
return ok(result);
|
|
2247
2216
|
} catch (e) {
|
|
2248
2217
|
return err2(toWriteError(e));
|
|
@@ -2253,7 +2222,7 @@ function createDb(options) {
|
|
|
2253
2222
|
return (async () => {
|
|
2254
2223
|
try {
|
|
2255
2224
|
const entry = resolveModel(models, name);
|
|
2256
|
-
const result = await update(
|
|
2225
|
+
const result = await update(qfn, entry.table, opts, dialectObj);
|
|
2257
2226
|
return ok(result);
|
|
2258
2227
|
} catch (e) {
|
|
2259
2228
|
return err2(toWriteError(e));
|
|
@@ -2264,7 +2233,7 @@ function createDb(options) {
|
|
|
2264
2233
|
return (async () => {
|
|
2265
2234
|
try {
|
|
2266
2235
|
const entry = resolveModel(models, name);
|
|
2267
|
-
const result = await updateMany(
|
|
2236
|
+
const result = await updateMany(qfn, entry.table, opts, dialectObj);
|
|
2268
2237
|
return ok(result);
|
|
2269
2238
|
} catch (e) {
|
|
2270
2239
|
return err2(toWriteError(e));
|
|
@@ -2275,7 +2244,7 @@ function createDb(options) {
|
|
|
2275
2244
|
return (async () => {
|
|
2276
2245
|
try {
|
|
2277
2246
|
const entry = resolveModel(models, name);
|
|
2278
|
-
const result = await upsert(
|
|
2247
|
+
const result = await upsert(qfn, entry.table, opts, dialectObj);
|
|
2279
2248
|
return ok(result);
|
|
2280
2249
|
} catch (e) {
|
|
2281
2250
|
return err2(toWriteError(e));
|
|
@@ -2286,7 +2255,7 @@ function createDb(options) {
|
|
|
2286
2255
|
return (async () => {
|
|
2287
2256
|
try {
|
|
2288
2257
|
const entry = resolveModel(models, name);
|
|
2289
|
-
const result = await deleteOne(
|
|
2258
|
+
const result = await deleteOne(qfn, entry.table, opts, dialectObj);
|
|
2290
2259
|
return ok(result);
|
|
2291
2260
|
} catch (e) {
|
|
2292
2261
|
return err2(toWriteError(e));
|
|
@@ -2297,7 +2266,7 @@ function createDb(options) {
|
|
|
2297
2266
|
return (async () => {
|
|
2298
2267
|
try {
|
|
2299
2268
|
const entry = resolveModel(models, name);
|
|
2300
|
-
const result = await deleteMany(
|
|
2269
|
+
const result = await deleteMany(qfn, entry.table, opts, dialectObj);
|
|
2301
2270
|
return ok(result);
|
|
2302
2271
|
} catch (e) {
|
|
2303
2272
|
return err2(toWriteError(e));
|
|
@@ -2308,7 +2277,7 @@ function createDb(options) {
|
|
|
2308
2277
|
return (async () => {
|
|
2309
2278
|
try {
|
|
2310
2279
|
const entry = resolveModel(models, name);
|
|
2311
|
-
const result = await count(
|
|
2280
|
+
const result = await count(qfn, entry.table, opts);
|
|
2312
2281
|
return ok(result);
|
|
2313
2282
|
} catch (e) {
|
|
2314
2283
|
return err2(toReadError(e));
|
|
@@ -2319,7 +2288,7 @@ function createDb(options) {
|
|
|
2319
2288
|
return (async () => {
|
|
2320
2289
|
try {
|
|
2321
2290
|
const entry = resolveModel(models, name);
|
|
2322
|
-
const result = await aggregate(
|
|
2291
|
+
const result = await aggregate(qfn, entry.table, opts);
|
|
2323
2292
|
return ok(result);
|
|
2324
2293
|
} catch (e) {
|
|
2325
2294
|
return err2(toReadError(e));
|
|
@@ -2330,16 +2299,16 @@ function createDb(options) {
|
|
|
2330
2299
|
return (async () => {
|
|
2331
2300
|
try {
|
|
2332
2301
|
const entry = resolveModel(models, name);
|
|
2333
|
-
const result = await groupBy(
|
|
2302
|
+
const result = await groupBy(qfn, entry.table, opts);
|
|
2334
2303
|
return ok(result);
|
|
2335
2304
|
} catch (e) {
|
|
2336
2305
|
return err2(toReadError(e));
|
|
2337
2306
|
}
|
|
2338
2307
|
})();
|
|
2339
2308
|
}
|
|
2340
|
-
const
|
|
2309
|
+
const delegates = {};
|
|
2341
2310
|
for (const name of Object.keys(models)) {
|
|
2342
|
-
|
|
2311
|
+
delegates[name] = {
|
|
2343
2312
|
get: (opts) => implGet(name, opts),
|
|
2344
2313
|
getOrThrow: (opts) => implGetRequired(name, opts),
|
|
2345
2314
|
list: (opts) => implList(name, opts),
|
|
@@ -2357,14 +2326,142 @@ function createDb(options) {
|
|
|
2357
2326
|
groupBy: (opts) => implGroupBy(name, opts)
|
|
2358
2327
|
};
|
|
2359
2328
|
}
|
|
2360
|
-
|
|
2329
|
+
return delegates;
|
|
2330
|
+
}
|
|
2331
|
+
function buildQueryMethod(qfn) {
|
|
2332
|
+
return async (fragment) => {
|
|
2361
2333
|
try {
|
|
2362
|
-
const result = await executeQuery(
|
|
2334
|
+
const result = await executeQuery(qfn, fragment.sql, fragment.params);
|
|
2363
2335
|
return ok(result);
|
|
2364
2336
|
} catch (e) {
|
|
2365
2337
|
return err2(toReadError(e, fragment.sql));
|
|
2366
2338
|
}
|
|
2367
2339
|
};
|
|
2340
|
+
}
|
|
2341
|
+
function createDb(options) {
|
|
2342
|
+
const { models, log, dialect } = options;
|
|
2343
|
+
for (const key of Object.keys(models)) {
|
|
2344
|
+
if (RESERVED_MODEL_NAMES.has(key)) {
|
|
2345
|
+
throw new Error(`Model name "${key}" is reserved. Choose a different name for this model. ` + `Reserved names: ${[...RESERVED_MODEL_NAMES].join(", ")}`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (dialect === "sqlite") {
|
|
2349
|
+
if (!options.d1) {
|
|
2350
|
+
throw new Error("SQLite dialect requires a D1 binding");
|
|
2351
|
+
}
|
|
2352
|
+
if (options.url) {
|
|
2353
|
+
throw new Error("SQLite dialect uses D1, not a connection URL");
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
const dialectObj = dialect === "sqlite" ? defaultSqliteDialect : defaultPostgresDialect;
|
|
2357
|
+
const tenantGraph = computeTenantGraph(models);
|
|
2358
|
+
if (log && tenantGraph.root !== null) {
|
|
2359
|
+
const allScoped = new Set([
|
|
2360
|
+
...tenantGraph.root !== null ? [tenantGraph.root] : [],
|
|
2361
|
+
...tenantGraph.directlyScoped,
|
|
2362
|
+
...tenantGraph.indirectlyScoped,
|
|
2363
|
+
...tenantGraph.shared
|
|
2364
|
+
]);
|
|
2365
|
+
for (const [key, entry] of Object.entries(models)) {
|
|
2366
|
+
if (!allScoped.has(key)) {
|
|
2367
|
+
log(`[vertz/db] Table "${entry.table._name}" has no tenant path and is not marked .shared(). ` + "It will not be automatically scoped to a tenant.");
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
const modelsRegistry = models;
|
|
2372
|
+
let driver = null;
|
|
2373
|
+
let sqliteDriver = null;
|
|
2374
|
+
let replicaDrivers = [];
|
|
2375
|
+
let replicaIndex = 0;
|
|
2376
|
+
let initPostgres = null;
|
|
2377
|
+
const queryFn = (() => {
|
|
2378
|
+
if (options._queryFn) {
|
|
2379
|
+
return options._queryFn;
|
|
2380
|
+
}
|
|
2381
|
+
if (dialect === "sqlite" && options.d1) {
|
|
2382
|
+
const tableSchema = buildTableSchema(models);
|
|
2383
|
+
sqliteDriver = createSqliteDriver(options.d1, tableSchema);
|
|
2384
|
+
return async (sqlStr, params) => {
|
|
2385
|
+
if (!sqliteDriver) {
|
|
2386
|
+
throw new Error("SQLite driver not initialized");
|
|
2387
|
+
}
|
|
2388
|
+
const rows = await sqliteDriver.query(sqlStr, params);
|
|
2389
|
+
return { rows, rowCount: rows.length };
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
if (options.url) {
|
|
2393
|
+
let initialized = false;
|
|
2394
|
+
initPostgres = async () => {
|
|
2395
|
+
if (initialized)
|
|
2396
|
+
return;
|
|
2397
|
+
const { createPostgresDriver } = await import("./shared/chunk-p2x2vmg5.js");
|
|
2398
|
+
driver = createPostgresDriver(options.url, options.pool);
|
|
2399
|
+
const replicas = options.pool?.replicas;
|
|
2400
|
+
if (replicas && replicas.length > 0) {
|
|
2401
|
+
replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
|
|
2402
|
+
}
|
|
2403
|
+
initialized = true;
|
|
2404
|
+
};
|
|
2405
|
+
return async (sqlStr, params) => {
|
|
2406
|
+
await initPostgres?.();
|
|
2407
|
+
if (replicaDrivers.length === 0) {
|
|
2408
|
+
if (!driver) {
|
|
2409
|
+
throw new Error("Database driver not initialized");
|
|
2410
|
+
}
|
|
2411
|
+
return driver.queryFn(sqlStr, params);
|
|
2412
|
+
}
|
|
2413
|
+
if (isReadQuery(sqlStr)) {
|
|
2414
|
+
const targetReplica = replicaDrivers[replicaIndex];
|
|
2415
|
+
replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
|
|
2416
|
+
try {
|
|
2417
|
+
return await targetReplica.queryFn(sqlStr, params);
|
|
2418
|
+
} catch (err3) {
|
|
2419
|
+
console.warn("[vertz/db] replica query failed, falling back to primary:", err3.message);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (!driver) {
|
|
2423
|
+
throw new Error("Database driver not initialized");
|
|
2424
|
+
}
|
|
2425
|
+
return driver.queryFn(sqlStr, params);
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
return async () => {
|
|
2429
|
+
throw new Error("db.query() requires a connected database driver. Provide a `url` to connect to PostgreSQL, a `dialect` with D1 binding for SQLite, or `_queryFn` for testing.");
|
|
2430
|
+
};
|
|
2431
|
+
})();
|
|
2432
|
+
const delegates = buildDelegates(queryFn, models, dialectObj, modelsRegistry);
|
|
2433
|
+
const client = { ...delegates };
|
|
2434
|
+
client.query = buildQueryMethod(queryFn);
|
|
2435
|
+
client.transaction = async (fn) => {
|
|
2436
|
+
if (initPostgres) {
|
|
2437
|
+
await initPostgres();
|
|
2438
|
+
}
|
|
2439
|
+
if (driver?.beginTransaction) {
|
|
2440
|
+
return await driver.beginTransaction(async (txQueryFn) => {
|
|
2441
|
+
const txDelegates = buildDelegates(txQueryFn, models, dialectObj, modelsRegistry);
|
|
2442
|
+
const tx = {
|
|
2443
|
+
...txDelegates,
|
|
2444
|
+
query: buildQueryMethod(txQueryFn)
|
|
2445
|
+
};
|
|
2446
|
+
return fn(tx);
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
await queryFn("BEGIN", []);
|
|
2450
|
+
try {
|
|
2451
|
+
const tx = {
|
|
2452
|
+
...delegates,
|
|
2453
|
+
query: client.query
|
|
2454
|
+
};
|
|
2455
|
+
const result = await fn(tx);
|
|
2456
|
+
await queryFn("COMMIT", []);
|
|
2457
|
+
return result;
|
|
2458
|
+
} catch (e) {
|
|
2459
|
+
try {
|
|
2460
|
+
await queryFn("ROLLBACK", []);
|
|
2461
|
+
} catch {}
|
|
2462
|
+
throw e;
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2368
2465
|
client.close = async () => {
|
|
2369
2466
|
if (driver) {
|
|
2370
2467
|
await driver.close();
|
package/dist/postgres/index.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ interface DbDriver {
|
|
|
29
29
|
rowsAffected: number;
|
|
30
30
|
}>;
|
|
31
31
|
/**
|
|
32
|
+
* Execute a callback within a database transaction.
|
|
33
|
+
* The callback receives a transaction-scoped QueryFn.
|
|
34
|
+
* Optional — not all drivers support transactions (e.g., D1).
|
|
35
|
+
*/
|
|
36
|
+
beginTransaction?<T>(fn: (txQueryFn: QueryFn) => Promise<T>): Promise<T>;
|
|
37
|
+
/**
|
|
32
38
|
* Close the database connection.
|
|
33
39
|
*/
|
|
34
40
|
close(): Promise<void>;
|