@vertz/db 0.2.15 → 0.2.16

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/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  buildSelect,
5
5
  buildUpdate,
6
6
  buildWhere
7
- } from "./shared/chunk-pxjcpnpx.js";
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-ndxe1h28.js";
50
+ } from "./shared/chunk-tnaf4hbj.js";
51
51
  import {
52
52
  generateId
53
- } from "./shared/chunk-pkv8w501.js";
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({ where: { id } });
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 dbOptions = {};
67
- if (options?.where) {
68
- dbOptions.where = options.where;
69
- }
70
- if (options?.orderBy) {
71
- dbOptions.orderBy = options.orderBy;
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
- async function loadRelations(queryFn, primaryRows, relations, include, depth = 0, tablesRegistry, primaryTable) {
1618
- if (depth > 2 || primaryRows.length === 0) {
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: { [targetPk]: { in: [...fkValues] } }
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 < 2) {
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: { [fk]: { in: [...pkValues] } }
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
- if (typeof includeValue === "object" && includeValue.include && depth < 2) {
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: { [targetPk]: { in: [...allTargetIds] } }
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 < 2) {
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 createDb(options) {
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(queryFn, entry.table, opts, dialectObj);
2125
+ const result = await get(qfn, entry.table, opts, dialectObj);
2157
2126
  if (result !== null && opts?.include) {
2158
- const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
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(queryFn, entry.table, opts, dialectObj);
2162
+ const results = await list(qfn, entry.table, opts, dialectObj);
2194
2163
  if (opts?.include && results.length > 0) {
2195
- const withRelations = await loadRelations(queryFn, results, entry.relations, opts.include, 0, modelsRegistry, entry.table);
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(queryFn, entry.table, opts, dialectObj);
2177
+ const { data, total } = await listAndCount(qfn, entry.table, opts, dialectObj);
2209
2178
  if (opts?.include && data.length > 0) {
2210
- const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, modelsRegistry, entry.table);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts, dialectObj);
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(queryFn, entry.table, opts);
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(queryFn, entry.table, opts);
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(queryFn, entry.table, opts);
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 client = {};
2309
+ const delegates = {};
2341
2310
  for (const name of Object.keys(models)) {
2342
- client[name] = {
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
- client.query = async (fragment) => {
2329
+ return delegates;
2330
+ }
2331
+ function buildQueryMethod(qfn) {
2332
+ return async (fragment) => {
2361
2333
  try {
2362
- const result = await executeQuery(queryFn, fragment.sql, fragment.params);
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();
@@ -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>;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createPostgresDriver
3
- } from "../shared/chunk-rqe0prft.js";
3
+ } from "../shared/chunk-rjry8vc2.js";
4
4
  import"../shared/chunk-j4kwq1gh.js";
5
5
  export {
6
6
  createPostgresDriver