@rebasepro/server-postgres 0.9.1-canary.16c42e9 → 0.9.1-canary.1d2d8b5
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/PostgresBackendDriver.d.ts +2 -25
- package/dist/PostgresBootstrapper.d.ts +0 -10
- package/dist/auth/services.d.ts +0 -16
- package/dist/connection.d.ts +0 -21
- package/dist/data-transformer.d.ts +2 -9
- package/dist/index.es.js +548 -1310
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-default-policies.d.ts +10 -0
- package/dist/services/FetchService.d.ts +24 -4
- package/dist/services/PersistService.d.ts +1 -9
- package/dist/services/RelationService.d.ts +1 -34
- package/dist/services/collection-helpers.d.ts +14 -79
- package/dist/services/dataService.d.ts +1 -3
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +0 -7
- package/package.json +8 -10
- package/src/PostgresBackendDriver.ts +13 -127
- package/src/PostgresBootstrapper.ts +25 -62
- package/src/auth/ensure-tables.ts +11 -73
- package/src/auth/services.ts +19 -49
- package/src/connection.ts +1 -61
- package/src/data-transformer.ts +9 -11
- package/src/databasePoolManager.ts +0 -2
- package/src/schema/auth-default-policies.ts +132 -0
- package/src/schema/generate-drizzle-schema-logic.ts +29 -24
- package/src/schema/generate-postgres-ddl-logic.ts +28 -76
- package/src/schema/introspect-db.ts +2 -19
- package/src/services/BranchService.ts +10 -42
- package/src/services/FetchService.ts +270 -65
- package/src/services/PersistService.ts +9 -62
- package/src/services/RelationService.ts +94 -153
- package/src/services/collection-helpers.ts +47 -164
- package/src/services/dataService.ts +2 -3
- package/src/services/index.ts +0 -1
- package/src/services/realtimeService.ts +19 -40
- package/src/utils/drizzle-conditions.ts +0 -13
- package/src/websocket.ts +1 -4
- package/dist/collections/buildRegistry.d.ts +0 -27
- package/dist/services/row-pipeline.d.ts +0 -63
- package/src/collections/buildRegistry.ts +0 -59
- package/src/services/row-pipeline.ts +0 -215
package/dist/index.es.js
CHANGED
|
@@ -6,12 +6,12 @@ import { drizzle } from "drizzle-orm/node-postgres";
|
|
|
6
6
|
import { ApiError, createEmailService, extractUserFromToken, loadCollectionsFromDirectory, logger, safeCompare } from "@rebasepro/server";
|
|
7
7
|
import { and, asc, count, desc, eq, getTableColumns, getTableName, gt, ilike, inArray, isTable, lt, or, relations, sql } from "drizzle-orm";
|
|
8
8
|
import { PgArray, PgChar, PgTable, PgText, PgVarchar, bigint, boolean, char, cidr, customType, date, doublePrecision, geometry, getTableConfig, inet, integer, interval, json, jsonb, line, macaddr, macaddr8, numeric, pgSchema, pgTable, point, primaryKey, real, smallint, text, time, timestamp, unique, uuid, varchar, vector } from "drizzle-orm/pg-core";
|
|
9
|
+
import { createHash, randomUUID } from "crypto";
|
|
9
10
|
import fs, { promises } from "fs";
|
|
10
11
|
import path from "path";
|
|
11
12
|
import chokidar from "chokidar";
|
|
12
13
|
import { WebSocket, WebSocketServer } from "ws";
|
|
13
14
|
import { EventEmitter } from "events";
|
|
14
|
-
import { randomUUID } from "crypto";
|
|
15
15
|
import { inspect } from "util";
|
|
16
16
|
import os from "os";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -71,51 +71,16 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
71
71
|
var connection_exports = /* @__PURE__ */ __exportAll({
|
|
72
72
|
createDirectDatabaseConnection: () => createDirectDatabaseConnection,
|
|
73
73
|
createPostgresDatabaseConnection: () => createPostgresDatabaseConnection,
|
|
74
|
-
createReadReplicaConnection: () => createReadReplicaConnection
|
|
75
|
-
guardPoolAgainstDirtyRelease: () => guardPoolAgainstDirtyRelease
|
|
74
|
+
createReadReplicaConnection: () => createReadReplicaConnection
|
|
76
75
|
});
|
|
77
76
|
var DEFAULT_POOL = {
|
|
78
77
|
max: 20,
|
|
79
78
|
idleTimeoutMillis: 3e4,
|
|
80
79
|
connectionTimeoutMillis: 1e4,
|
|
81
|
-
queryTimeout:
|
|
80
|
+
queryTimeout: 3e4,
|
|
82
81
|
statementTimeout: 3e4,
|
|
83
82
|
keepAlive: true
|
|
84
83
|
};
|
|
85
|
-
/** ReadyForQuery status byte: `I` idle, `T` in transaction, `E` failed transaction. */
|
|
86
|
-
var TX_IDLE = "I";
|
|
87
|
-
/**
|
|
88
|
-
* Destroy pool clients that are released while still inside a transaction.
|
|
89
|
-
*
|
|
90
|
-
* pg-pool returns a client to the idle list whenever `release()` is called
|
|
91
|
-
* without an error — even if the connection is still mid-transaction (status
|
|
92
|
-
* `T`/`E`). That happens in practice: drizzle's pool transaction releases in
|
|
93
|
-
* a `finally` after attempting ROLLBACK, and if the ROLLBACK itself fails
|
|
94
|
-
* (e.g. it was queued behind a statement that hit the client-side
|
|
95
|
-
* query_timeout), the client goes back dirty. The next checkout then runs
|
|
96
|
-
* its statements inside the zombie transaction — with the previous request's
|
|
97
|
-
* `app.*` RLS GUCs still applied, which turns unrelated queries into
|
|
98
|
-
* RLS-scoped ones (observed in production as registration failing with
|
|
99
|
-
* SQLSTATE 42501 under a leaked anonymous context).
|
|
100
|
-
*
|
|
101
|
-
* pg-pool emits `release` before it consults its private `_expired` set, so
|
|
102
|
-
* marking the client expired here makes `_release()` destroy it instead of
|
|
103
|
-
* pooling it. Both `client._txStatus` (pg ≥ 8.16) and `pool._expired` are
|
|
104
|
-
* private APIs — feature-detect and fall back to loud logging so an upstream
|
|
105
|
-
* change degrades to observability, never to silent corruption.
|
|
106
|
-
*/
|
|
107
|
-
function guardPoolAgainstDirtyRelease(pool, label) {
|
|
108
|
-
pool.on("release", (err, client) => {
|
|
109
|
-
if (err) return;
|
|
110
|
-
const txStatus = client?._txStatus;
|
|
111
|
-
if (typeof txStatus !== "string" || txStatus === TX_IDLE) return;
|
|
112
|
-
const expired = pool._expired;
|
|
113
|
-
if (expired && typeof expired.add === "function" && typeof expired.has === "function" && client && typeof client === "object") {
|
|
114
|
-
expired.add(client);
|
|
115
|
-
logger.error(`[${label}] Client released back to the pool while still in a transaction (status '${txStatus}') — destroying it so the open transaction and its session state (RLS GUCs) cannot leak into the next request.`);
|
|
116
|
-
} else logger.error(`[${label}] Client released mid-transaction (status '${txStatus}') but the pool's internal expiry set is unavailable (pg-pool internals changed?). The connection may leak its open transaction into subsequent requests.`);
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
84
|
/**
|
|
120
85
|
* Create a Drizzle-backed Postgres connection with a production-grade
|
|
121
86
|
* connection pool.
|
|
@@ -147,7 +112,6 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
|
|
|
147
112
|
logger.error("[pg-pool] Unexpected pool error", { detail: err.message });
|
|
148
113
|
if (err.message.includes("ETIMEDOUT")) logger.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
|
|
149
114
|
});
|
|
150
|
-
guardPoolAgainstDirtyRelease(pool, "pg-pool");
|
|
151
115
|
return {
|
|
152
116
|
db: schema ? drizzle(pool, { schema }) : drizzle(pool),
|
|
153
117
|
pool,
|
|
@@ -180,7 +144,6 @@ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
|
180
144
|
pool.on("error", (err) => {
|
|
181
145
|
logger.error("[pg-direct-pool] Unexpected pool error", { detail: err.message });
|
|
182
146
|
});
|
|
183
|
-
guardPoolAgainstDirtyRelease(pool, "pg-direct-pool");
|
|
184
147
|
return {
|
|
185
148
|
db: schema ? drizzle(pool, { schema }) : drizzle(pool),
|
|
186
149
|
pool,
|
|
@@ -210,7 +173,6 @@ function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
|
210
173
|
pool.on("error", (err) => {
|
|
211
174
|
logger.error("[pg-replica-pool] Unexpected pool error", { detail: err.message });
|
|
212
175
|
});
|
|
213
|
-
guardPoolAgainstDirtyRelease(pool, "pg-replica-pool");
|
|
214
176
|
return {
|
|
215
177
|
db: schema ? drizzle(pool, { schema }) : drizzle(pool),
|
|
216
178
|
pool,
|
|
@@ -1540,140 +1502,6 @@ function removeFunctions(o) {
|
|
|
1540
1502
|
return o;
|
|
1541
1503
|
}
|
|
1542
1504
|
//#endregion
|
|
1543
|
-
//#region ../utils/src/sha1.ts
|
|
1544
|
-
/**
|
|
1545
|
-
* Minimal SHA-1 implementation that runs in both Node and the browser.
|
|
1546
|
-
*
|
|
1547
|
-
* This exists because generated Postgres policy names embed a SHA-1 digest of
|
|
1548
|
-
* the security rule. The DDL generator runs on the server (where `node:crypto`
|
|
1549
|
-
* is available) but the Studio has to derive the same names in the browser to
|
|
1550
|
-
* tell a policy it generated apart from one it did not. `node:crypto` cannot be
|
|
1551
|
-
* bundled for the browser, so the shared derivation needs a portable digest.
|
|
1552
|
-
*
|
|
1553
|
-
* SHA-1 is used purely to name things deterministically — never for security.
|
|
1554
|
-
* The output is byte-identical to `createHash("sha1").update(str).digest("hex")`,
|
|
1555
|
-
* which `sha1.test.ts` pins against `node:crypto` directly.
|
|
1556
|
-
*/
|
|
1557
|
-
/** Rotate a 32-bit word left by `n` bits. */
|
|
1558
|
-
function rotl(value, n) {
|
|
1559
|
-
return value << n | value >>> 32 - n;
|
|
1560
|
-
}
|
|
1561
|
-
/**
|
|
1562
|
-
* SHA-1 digest of a string, hex-encoded.
|
|
1563
|
-
*
|
|
1564
|
-
* The input is encoded as UTF-8, matching Node's default handling of strings
|
|
1565
|
-
* passed to `hash.update(str)`.
|
|
1566
|
-
*/
|
|
1567
|
-
function sha1Hex(input) {
|
|
1568
|
-
const bytes = Array.from(new TextEncoder().encode(input));
|
|
1569
|
-
const bitLength = bytes.length * 8;
|
|
1570
|
-
bytes.push(128);
|
|
1571
|
-
while (bytes.length % 64 !== 56) bytes.push(0);
|
|
1572
|
-
const hi = Math.floor(bitLength / 4294967296);
|
|
1573
|
-
const lo = bitLength >>> 0;
|
|
1574
|
-
bytes.push(hi >>> 24 & 255, hi >>> 16 & 255, hi >>> 8 & 255, hi & 255);
|
|
1575
|
-
bytes.push(lo >>> 24 & 255, lo >>> 16 & 255, lo >>> 8 & 255, lo & 255);
|
|
1576
|
-
let h0 = 1732584193;
|
|
1577
|
-
let h1 = 4023233417;
|
|
1578
|
-
let h2 = 2562383102;
|
|
1579
|
-
let h3 = 271733878;
|
|
1580
|
-
let h4 = 3285377520;
|
|
1581
|
-
const w = new Array(80);
|
|
1582
|
-
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
1583
|
-
for (let i = 0; i < 16; i++) {
|
|
1584
|
-
const j = offset + i * 4;
|
|
1585
|
-
w[i] = bytes[j] << 24 | bytes[j + 1] << 16 | bytes[j + 2] << 8 | bytes[j + 3] | 0;
|
|
1586
|
-
}
|
|
1587
|
-
for (let i = 16; i < 80; i++) w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
1588
|
-
let a = h0;
|
|
1589
|
-
let b = h1;
|
|
1590
|
-
let c = h2;
|
|
1591
|
-
let d = h3;
|
|
1592
|
-
let e = h4;
|
|
1593
|
-
for (let i = 0; i < 80; i++) {
|
|
1594
|
-
let f;
|
|
1595
|
-
let k;
|
|
1596
|
-
if (i < 20) {
|
|
1597
|
-
f = b & c | ~b & d;
|
|
1598
|
-
k = 1518500249;
|
|
1599
|
-
} else if (i < 40) {
|
|
1600
|
-
f = b ^ c ^ d;
|
|
1601
|
-
k = 1859775393;
|
|
1602
|
-
} else if (i < 60) {
|
|
1603
|
-
f = b & c | b & d | c & d;
|
|
1604
|
-
k = 2400959708;
|
|
1605
|
-
} else {
|
|
1606
|
-
f = b ^ c ^ d;
|
|
1607
|
-
k = 3395469782;
|
|
1608
|
-
}
|
|
1609
|
-
const temp = rotl(a, 5) + f + e + k + w[i] | 0;
|
|
1610
|
-
e = d;
|
|
1611
|
-
d = c;
|
|
1612
|
-
c = rotl(b, 30);
|
|
1613
|
-
b = a;
|
|
1614
|
-
a = temp;
|
|
1615
|
-
}
|
|
1616
|
-
h0 = h0 + a | 0;
|
|
1617
|
-
h1 = h1 + b | 0;
|
|
1618
|
-
h2 = h2 + c | 0;
|
|
1619
|
-
h3 = h3 + d | 0;
|
|
1620
|
-
h4 = h4 + e | 0;
|
|
1621
|
-
}
|
|
1622
|
-
return [
|
|
1623
|
-
h0,
|
|
1624
|
-
h1,
|
|
1625
|
-
h2,
|
|
1626
|
-
h3,
|
|
1627
|
-
h4
|
|
1628
|
-
].map((word) => (word >>> 0).toString(16).padStart(8, "0")).join("");
|
|
1629
|
-
}
|
|
1630
|
-
//#endregion
|
|
1631
|
-
//#region ../utils/src/policy-names.ts
|
|
1632
|
-
/**
|
|
1633
|
-
* Naming of the Postgres policies generated from a collection's security rules.
|
|
1634
|
-
*
|
|
1635
|
-
* A rule without an explicit `name` is compiled to `<table>_<op>_<hash>`, where
|
|
1636
|
-
* the hash covers the rule's semantics. The Studio needs the same names to tell
|
|
1637
|
-
* "this policy came from your code" apart from "someone wrote this in SQL" —
|
|
1638
|
-
* without them it treats generated policies as foreign and offers to import
|
|
1639
|
-
* them back into the codebase they came from.
|
|
1640
|
-
*
|
|
1641
|
-
* This is the single definition of that naming. The DDL and Drizzle generators
|
|
1642
|
-
* both derive names from here, so a change cannot silently rename every policy
|
|
1643
|
-
* in every deployed database while the UI keeps matching the old ones.
|
|
1644
|
-
*/
|
|
1645
|
-
/** Stable digest of the parts of a rule that determine what the policy does. */
|
|
1646
|
-
function getPolicyNameHash(rule) {
|
|
1647
|
-
return sha1Hex(JSON.stringify({
|
|
1648
|
-
a: rule.access,
|
|
1649
|
-
m: rule.mode,
|
|
1650
|
-
op: rule.operation,
|
|
1651
|
-
ops: rule.operations?.slice().sort(),
|
|
1652
|
-
own: rule.ownerField,
|
|
1653
|
-
rol: rule.roles?.slice().sort(),
|
|
1654
|
-
pg: rule.pgRoles?.slice().sort(),
|
|
1655
|
-
u: rule.using,
|
|
1656
|
-
w: rule.withCheck,
|
|
1657
|
-
c: rule.condition,
|
|
1658
|
-
ch: rule.check
|
|
1659
|
-
})).substring(0, 7);
|
|
1660
|
-
}
|
|
1661
|
-
/** The operations a rule expands to — `operations` wins over `operation`. */
|
|
1662
|
-
function getPolicyOperations(rule) {
|
|
1663
|
-
return rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
1664
|
-
}
|
|
1665
|
-
/**
|
|
1666
|
-
* Every Postgres policy name a single rule compiles to — one per operation.
|
|
1667
|
-
*
|
|
1668
|
-
* @param rule The security rule as written in the collection config.
|
|
1669
|
-
* @param tableName The rule's table (see `getTableName` in `@rebasepro/common`).
|
|
1670
|
-
*/
|
|
1671
|
-
function getPolicyNamesForRule(rule, tableName) {
|
|
1672
|
-
const ops = getPolicyOperations(rule);
|
|
1673
|
-
const ruleHash = getPolicyNameHash(rule);
|
|
1674
|
-
return ops.map((op, opIdx) => rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`);
|
|
1675
|
-
}
|
|
1676
|
-
//#endregion
|
|
1677
1505
|
//#region ../utils/src/names.ts
|
|
1678
1506
|
/**
|
|
1679
1507
|
* Generates a foreign key column name from a given string, typically a collection slug or name.
|
|
@@ -1798,109 +1626,6 @@ function createRelationRefWithData(id, path, data) {
|
|
|
1798
1626
|
data
|
|
1799
1627
|
};
|
|
1800
1628
|
}
|
|
1801
|
-
/**
|
|
1802
|
-
* Derive a row's address from its key columns.
|
|
1803
|
-
*
|
|
1804
|
-
* Single key → the value as a string. Composite → each part joined by
|
|
1805
|
-
* {@link COMPOSITE_ID_SEPARATOR}, in primary-key order, which is what
|
|
1806
|
-
* {@link parseIdValues} expects to invert.
|
|
1807
|
-
*/
|
|
1808
|
-
function buildCompositeId(values, primaryKeys) {
|
|
1809
|
-
if (primaryKeys.length === 0) return "";
|
|
1810
|
-
if (primaryKeys.length === 1) return String(values[primaryKeys[0].fieldName] ?? "");
|
|
1811
|
-
return primaryKeys.map((pk) => String(values[pk.fieldName] ?? "")).join(":::");
|
|
1812
|
-
}
|
|
1813
|
-
/**
|
|
1814
|
-
* Invert {@link buildCompositeId}: turn an address back into key columns, each
|
|
1815
|
-
* coerced to the type its column actually round-trips as.
|
|
1816
|
-
*
|
|
1817
|
-
* This is the boundary where a URL segment becomes a query parameter, so a
|
|
1818
|
-
* malformed address must throw rather than silently produce a query that
|
|
1819
|
-
* matches the wrong row (or none).
|
|
1820
|
-
*/
|
|
1821
|
-
function parseIdValues(idValue, primaryKeys) {
|
|
1822
|
-
const result = {};
|
|
1823
|
-
if (primaryKeys.length === 0) return result;
|
|
1824
|
-
if (primaryKeys.length === 1) {
|
|
1825
|
-
const pk = primaryKeys[0];
|
|
1826
|
-
if (pk.type === "number" && !pk.isUUID) {
|
|
1827
|
-
const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
|
|
1828
|
-
if (isNaN(parsed)) throw new Error(`Invalid numeric ID: ${idValue}`);
|
|
1829
|
-
result[pk.fieldName] = parsed;
|
|
1830
|
-
} else result[pk.fieldName] = String(idValue);
|
|
1831
|
-
return result;
|
|
1832
|
-
}
|
|
1833
|
-
const parts = String(idValue).split(":::");
|
|
1834
|
-
if (parts.length !== primaryKeys.length) throw new Error(`Composite ID parts mismatch. Expected ${primaryKeys.length}, got ${parts.length} for ID: ${idValue}`);
|
|
1835
|
-
for (let i = 0; i < primaryKeys.length; i++) {
|
|
1836
|
-
const pk = primaryKeys[i];
|
|
1837
|
-
const val = parts[i];
|
|
1838
|
-
if (pk.type === "number" && !pk.isUUID) {
|
|
1839
|
-
const parsed = parseInt(val, 10);
|
|
1840
|
-
if (isNaN(parsed)) throw new Error(`Invalid numeric ID component: ${val}`);
|
|
1841
|
-
result[pk.fieldName] = parsed;
|
|
1842
|
-
} else result[pk.fieldName] = val;
|
|
1843
|
-
}
|
|
1844
|
-
return result;
|
|
1845
|
-
}
|
|
1846
|
-
/**
|
|
1847
|
-
* The primary keys of a collection, as declared by its properties.
|
|
1848
|
-
*
|
|
1849
|
-
* This is the only tier both sides can read, because it is the only one written
|
|
1850
|
-
* in the config: the postgres driver can also infer keys from the Drizzle
|
|
1851
|
-
* schema, which the browser never sees and is never sent — the admin compiles
|
|
1852
|
-
* the collection files into its own bundle rather than being served them. A key
|
|
1853
|
-
* that lives only in the Drizzle schema is therefore invisible here, and the
|
|
1854
|
-
* server says so at boot (`warnOnKeysTheAdminCannotResolve`) naming the `isId`
|
|
1855
|
-
* to add.
|
|
1856
|
-
*
|
|
1857
|
-
* Returns an empty array when a collection declares none, which callers must
|
|
1858
|
-
* treat as "not addressable" rather than defaulting to `id`: guessing a key
|
|
1859
|
-
* that is not the real one produces confidently wrong addresses.
|
|
1860
|
-
*/
|
|
1861
|
-
function getDeclaredPrimaryKeys(collection) {
|
|
1862
|
-
const properties = collection.properties;
|
|
1863
|
-
if (!properties) return [];
|
|
1864
|
-
const keys = [];
|
|
1865
|
-
for (const [fieldName, propRaw] of Object.entries(properties)) {
|
|
1866
|
-
const prop = propRaw;
|
|
1867
|
-
if (!prop || typeof prop !== "object") continue;
|
|
1868
|
-
if (!("isId" in prop) || !prop.isId) continue;
|
|
1869
|
-
keys.push({
|
|
1870
|
-
fieldName,
|
|
1871
|
-
type: prop.type === "number" ? "number" : "string",
|
|
1872
|
-
isUUID: prop.isId === "uuid"
|
|
1873
|
-
});
|
|
1874
|
-
}
|
|
1875
|
-
return keys;
|
|
1876
|
-
}
|
|
1877
|
-
/**
|
|
1878
|
-
* The keys to address a collection's rows with, resolved the way the driver
|
|
1879
|
-
* resolves them — minus the tier the browser cannot reach.
|
|
1880
|
-
*
|
|
1881
|
-
* The postgres driver tries, in order: properties marked `isId`; the primary
|
|
1882
|
-
* keys of the Drizzle schema; and finally a column literally named `id`. Only
|
|
1883
|
-
* the first and last are visible in a `CollectionConfig`, which is what both
|
|
1884
|
-
* sides share.
|
|
1885
|
-
*
|
|
1886
|
-
* So the two agree except on a collection that declares no `isId` and whose key
|
|
1887
|
-
* is known only to Drizzle. There, the driver reads the real key, and this
|
|
1888
|
-
* either resolves nothing (reported to the console by the caller) or — if the
|
|
1889
|
-
* table happens to have an unrelated `id` property — resolves `id`, which is
|
|
1890
|
-
* the wrong key and cannot be detected from here: the addresses look right and
|
|
1891
|
-
* route wrong. Only the config can settle it, so the server names both cases
|
|
1892
|
-
* at boot (`warnOnKeysTheAdminCannotResolve`) with the `isId` to add.
|
|
1893
|
-
*/
|
|
1894
|
-
function resolvePrimaryKeys(collection) {
|
|
1895
|
-
const declared = getDeclaredPrimaryKeys(collection);
|
|
1896
|
-
if (declared.length > 0) return declared;
|
|
1897
|
-
const idProp = collection.properties?.id;
|
|
1898
|
-
if (idProp && typeof idProp === "object") return [{
|
|
1899
|
-
fieldName: "id",
|
|
1900
|
-
type: idProp.type === "number" ? "number" : "string"
|
|
1901
|
-
}];
|
|
1902
|
-
return [];
|
|
1903
|
-
}
|
|
1904
1629
|
//#endregion
|
|
1905
1630
|
//#region ../common/src/util/enums.ts
|
|
1906
1631
|
function enumToObjectEntries(enumValues) {
|
|
@@ -2559,307 +2284,6 @@ var buildPropertyCallbacks = (properties) => {
|
|
|
2559
2284
|
};
|
|
2560
2285
|
return Object.keys(propertyCallbacks).length > 0 ? propertyCallbacks : void 0;
|
|
2561
2286
|
};
|
|
2562
|
-
//#endregion
|
|
2563
|
-
//#region ../common/src/util/auth-default-policies.ts
|
|
2564
|
-
/**
|
|
2565
|
-
* Default RLS policies injected by the schema generator.
|
|
2566
|
-
*
|
|
2567
|
-
* Rebase's enforcement model is unified: authenticated (user-context) requests
|
|
2568
|
-
* run under the restricted `rebase_user` role, so Postgres RLS binds *every*
|
|
2569
|
-
* statement — reads and writes. A collection's `securityRules` are the whole
|
|
2570
|
-
* authorization model. The server context (auth flows, migrations,
|
|
2571
|
-
* `dataAsAdmin`) runs as the owner and bypasses RLS.
|
|
2572
|
-
*
|
|
2573
|
-
* Because RLS default-denies, every collection is **locked by default**: with
|
|
2574
|
-
* no rules, only the server context and admins can touch it. The generator
|
|
2575
|
-
* injects that safe baseline:
|
|
2576
|
-
*
|
|
2577
|
-
* **For every collection**
|
|
2578
|
-
* 1. A permissive **server-or-admin SELECT** grant.
|
|
2579
|
-
* 2. A permissive **server-or-admin write** grant (insert/update/delete).
|
|
2580
|
-
*
|
|
2581
|
-
* Author `securityRules` are permissive and OR together, so explicit rules only
|
|
2582
|
-
* *broaden* access from this locked baseline (e.g. "users read/write their own
|
|
2583
|
-
* rows").
|
|
2584
|
-
*
|
|
2585
|
-
* **For auth collections additionally**
|
|
2586
|
-
* 3. A permissive **self SELECT** grant (`id = auth.uid()`), so users can read
|
|
2587
|
-
* their own row (profile, session bootstrap) without every app re-declaring
|
|
2588
|
-
* it.
|
|
2589
|
-
* 4. A **restrictive** admin write gate. Restrictive policies are AND'd with
|
|
2590
|
-
* every other policy, so a write is rejected unless the caller is an admin
|
|
2591
|
-
* (or the server context) — even if the author also wrote a permissive rule
|
|
2592
|
-
* such as "a user may edit their own row". Without this, a permissive owner
|
|
2593
|
-
* rule would let a user change their own `roles`.
|
|
2594
|
-
*
|
|
2595
|
-
* The server context is recognised as `auth.uid() IS NULL` (`policy.serverContext()`)
|
|
2596
|
-
* — the built-in flows that run without a user (signup, migrations) set no user
|
|
2597
|
-
* GUC — which also lets the owner connection satisfy these policies even under
|
|
2598
|
-
* FORCE RLS. A *user* request never reaches that state: an anonymous one carries
|
|
2599
|
-
* `ANONYMOUS_USER_ID`, precisely so it cannot pass for the server here.
|
|
2600
|
-
*
|
|
2601
|
-
* Opt out with `disableDefaultPolicies: true` to take full responsibility for
|
|
2602
|
-
* the collection's RLS.
|
|
2603
|
-
*/
|
|
2604
|
-
var SERVER_OR_ADMIN_EXPR$1 = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
2605
|
-
/** Write operations that must be admin-gated by default on auth collections. */
|
|
2606
|
-
var DEFAULT_GUARDED_OPS = [
|
|
2607
|
-
"insert",
|
|
2608
|
-
"update",
|
|
2609
|
-
"delete"
|
|
2610
|
-
];
|
|
2611
|
-
/** Whether a collection is flagged as an authentication collection. */
|
|
2612
|
-
function isAuthCollection(collection) {
|
|
2613
|
-
const auth = collection.auth;
|
|
2614
|
-
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
2615
|
-
}
|
|
2616
|
-
/** The property marked as the row id (falls back to `id`). */
|
|
2617
|
-
function getIdPropertyName$1(collection) {
|
|
2618
|
-
for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
|
|
2619
|
-
return "id";
|
|
2620
|
-
}
|
|
2621
|
-
/**
|
|
2622
|
-
* Returns the security rules that should be applied to a collection: the
|
|
2623
|
-
* author's explicit `securityRules` plus the framework defaults described in
|
|
2624
|
-
* the module doc (baseline server/admin read for all collections; self-read
|
|
2625
|
-
* and the admin write gate for auth collections).
|
|
2626
|
-
*
|
|
2627
|
-
* Collections that opt out via `disableDefaultPolicies` are returned unchanged.
|
|
2628
|
-
*/
|
|
2629
|
-
function getEffectiveSecurityRules(collection) {
|
|
2630
|
-
const explicit = [...(isPostgresCollectionConfig(collection) ? collection.securityRules : void 0) ?? []];
|
|
2631
|
-
if (collection.disableDefaultPolicies) return explicit;
|
|
2632
|
-
const tableName = getTableName$1(collection);
|
|
2633
|
-
const injected = [];
|
|
2634
|
-
injected.push({
|
|
2635
|
-
name: `${tableName}_default_admin_read`,
|
|
2636
|
-
operations: ["select"],
|
|
2637
|
-
condition: SERVER_OR_ADMIN_EXPR$1
|
|
2638
|
-
});
|
|
2639
|
-
injected.push({
|
|
2640
|
-
name: `${tableName}_default_admin_write`,
|
|
2641
|
-
operations: [...DEFAULT_GUARDED_OPS],
|
|
2642
|
-
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2643
|
-
check: SERVER_OR_ADMIN_EXPR$1
|
|
2644
|
-
});
|
|
2645
|
-
if (isAuthCollection(collection)) {
|
|
2646
|
-
injected.push({
|
|
2647
|
-
name: `${tableName}_default_self_read`,
|
|
2648
|
-
operations: ["select"],
|
|
2649
|
-
condition: policy.compare(policy.field(getIdPropertyName$1(collection)), "eq", policy.authUid())
|
|
2650
|
-
});
|
|
2651
|
-
injected.push({
|
|
2652
|
-
name: `${tableName}_require_admin_write`,
|
|
2653
|
-
mode: "restrictive",
|
|
2654
|
-
operations: [...DEFAULT_GUARDED_OPS],
|
|
2655
|
-
condition: SERVER_OR_ADMIN_EXPR$1,
|
|
2656
|
-
check: SERVER_OR_ADMIN_EXPR$1
|
|
2657
|
-
});
|
|
2658
|
-
}
|
|
2659
|
-
return [...explicit, ...injected];
|
|
2660
|
-
}
|
|
2661
|
-
//#endregion
|
|
2662
|
-
//#region ../common/src/util/junction-policies.ts
|
|
2663
|
-
var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
2664
|
-
/**
|
|
2665
|
-
* Walk every collection's resolved relations and aggregate the junction tables
|
|
2666
|
-
* they declare. Two collections may declare the same junction from opposite
|
|
2667
|
-
* sides (posts→tags and tags→posts through `posts_tags`); both become
|
|
2668
|
-
* `declaringSides` of one spec, so derived write grants consider both.
|
|
2669
|
-
*/
|
|
2670
|
-
function resolveJunctionSpecs(collections) {
|
|
2671
|
-
const specs = /* @__PURE__ */ new Map();
|
|
2672
|
-
for (const collection of collections) {
|
|
2673
|
-
const resolved = resolveCollectionRelations(collection);
|
|
2674
|
-
for (const relation of Object.values(resolved)) {
|
|
2675
|
-
if (!relation.through) continue;
|
|
2676
|
-
const targetCollection = typeof relation.target === "function" ? relation.target() : void 0;
|
|
2677
|
-
if (!targetCollection) continue;
|
|
2678
|
-
const rawName = relation.through.table;
|
|
2679
|
-
const table = rawName.includes(".") ? rawName.split(".").pop() : rawName;
|
|
2680
|
-
const schema = "public";
|
|
2681
|
-
const source = {
|
|
2682
|
-
collection,
|
|
2683
|
-
junctionColumn: relation.through.sourceColumn,
|
|
2684
|
-
relation
|
|
2685
|
-
};
|
|
2686
|
-
const target = {
|
|
2687
|
-
collection: targetCollection,
|
|
2688
|
-
junctionColumn: relation.through.targetColumn
|
|
2689
|
-
};
|
|
2690
|
-
const existing = specs.get(table);
|
|
2691
|
-
if (!existing) specs.set(table, {
|
|
2692
|
-
table,
|
|
2693
|
-
schema,
|
|
2694
|
-
endpoints: [source, target],
|
|
2695
|
-
declaringSides: [source]
|
|
2696
|
-
});
|
|
2697
|
-
else if (!existing.declaringSides.some((s) => s.collection === collection)) existing.declaringSides.push(source);
|
|
2698
|
-
}
|
|
2699
|
-
}
|
|
2700
|
-
return specs;
|
|
2701
|
-
}
|
|
2702
|
-
/**
|
|
2703
|
-
* A synthetic CollectionConfig standing in for the junction during policy
|
|
2704
|
-
* compilation and naming. Its two FK columns carry explicit `columnName`s so
|
|
2705
|
-
* `outerField` operands resolve to the exact columns the CREATE TABLE emitted,
|
|
2706
|
-
* whatever their casing.
|
|
2707
|
-
*/
|
|
2708
|
-
function getJunctionCollectionConfig(spec) {
|
|
2709
|
-
const properties = {};
|
|
2710
|
-
for (const endpoint of spec.endpoints) properties[endpoint.junctionColumn] = {
|
|
2711
|
-
type: "string",
|
|
2712
|
-
columnName: endpoint.junctionColumn
|
|
2713
|
-
};
|
|
2714
|
-
return {
|
|
2715
|
-
slug: spec.table,
|
|
2716
|
-
name: spec.table,
|
|
2717
|
-
table: spec.table,
|
|
2718
|
-
schema: spec.schema,
|
|
2719
|
-
properties
|
|
2720
|
-
};
|
|
2721
|
-
}
|
|
2722
|
-
/** The property marked as the row id (falls back to `id`). */
|
|
2723
|
-
function getIdPropertyName(collection) {
|
|
2724
|
-
for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
|
|
2725
|
-
return "id";
|
|
2726
|
-
}
|
|
2727
|
-
/** `EXISTS (SELECT 1 FROM endpoint WHERE endpoint.pk = junction.fk [AND extra])`. */
|
|
2728
|
-
function existsEndpoint(endpoint, extra) {
|
|
2729
|
-
const correlation = policy.compare(policy.field(getIdPropertyName(endpoint.collection)), "eq", policy.outerField(endpoint.junctionColumn));
|
|
2730
|
-
return policy.existsIn({
|
|
2731
|
-
collection: endpoint.collection.slug,
|
|
2732
|
-
where: extra ? policy.and(correlation, extra) : correlation
|
|
2733
|
-
});
|
|
2734
|
-
}
|
|
2735
|
-
/**
|
|
2736
|
-
* Whether a parent-rule expression keeps its meaning when moved inside the
|
|
2737
|
-
* junction's `EXISTS` subquery — and the re-scoped copy if it does.
|
|
2738
|
-
*
|
|
2739
|
-
* Returns `null` when the rule cannot be embedded faithfully: `raw` SQL
|
|
2740
|
-
* anywhere (its `{column}` placeholders would bind to the junction), or an
|
|
2741
|
-
* `outerField` inside a nested `existsIn` (it would bind to the junction while
|
|
2742
|
-
* the author meant the parent, and no operand can express "the middle scope").
|
|
2743
|
-
* Top-level `outerField`s are rewritten to `field`, which is what they meant.
|
|
2744
|
-
*/
|
|
2745
|
-
function embedParentExpression(expr, depth = 0) {
|
|
2746
|
-
switch (expr.kind) {
|
|
2747
|
-
case "raw": return null;
|
|
2748
|
-
case "and":
|
|
2749
|
-
case "or": {
|
|
2750
|
-
const parts = [];
|
|
2751
|
-
for (const child of expr.operands) {
|
|
2752
|
-
const embedded = embedParentExpression(child, depth);
|
|
2753
|
-
if (!embedded) return null;
|
|
2754
|
-
parts.push(embedded);
|
|
2755
|
-
}
|
|
2756
|
-
return expr.kind === "and" ? policy.and(...parts) : policy.or(...parts);
|
|
2757
|
-
}
|
|
2758
|
-
case "not": {
|
|
2759
|
-
const embedded = embedParentExpression(expr.operand, depth);
|
|
2760
|
-
return embedded ? policy.not(embedded) : null;
|
|
2761
|
-
}
|
|
2762
|
-
case "existsIn": {
|
|
2763
|
-
const where = embedParentExpression(expr.where, depth + 1);
|
|
2764
|
-
return where ? policy.existsIn({
|
|
2765
|
-
collection: expr.collection,
|
|
2766
|
-
where
|
|
2767
|
-
}) : null;
|
|
2768
|
-
}
|
|
2769
|
-
case "compare": {
|
|
2770
|
-
const left = embedOperand(expr.left, depth);
|
|
2771
|
-
const right = embedOperand(expr.right, depth);
|
|
2772
|
-
if (!left || !right) return null;
|
|
2773
|
-
return {
|
|
2774
|
-
...expr,
|
|
2775
|
-
left,
|
|
2776
|
-
right
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
default: return expr;
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
/** Re-scope an operand, or return `null` if its binding cannot be preserved. */
|
|
2783
|
-
function embedOperand(operand, depth) {
|
|
2784
|
-
if (operand.kind === "outerField") {
|
|
2785
|
-
if (depth === 0) return policy.field(operand.name);
|
|
2786
|
-
return null;
|
|
2787
|
-
}
|
|
2788
|
-
return operand;
|
|
2789
|
-
}
|
|
2790
|
-
/** Does the rule cover the `update` operation? */
|
|
2791
|
-
function coversUpdate(rule) {
|
|
2792
|
-
return getPolicyOperations(rule).some((op) => op === "update" || op === "all");
|
|
2793
|
-
}
|
|
2794
|
-
/**
|
|
2795
|
-
* The full derived policy set for a junction table: the locked server/admin
|
|
2796
|
-
* baseline, the endpoint-visibility read grant, inherited write grants, and
|
|
2797
|
-
* inherited restrictive gates. Returns `[]` when every declaring collection set
|
|
2798
|
-
* `disableDefaultPolicies` — the junction is then the author's to police, and
|
|
2799
|
-
* stays locked (RLS is still enabled) until they write policies for it.
|
|
2800
|
-
*/
|
|
2801
|
-
function getJunctionSecurityRules(spec) {
|
|
2802
|
-
if (spec.declaringSides.every((side) => side.collection.disableDefaultPolicies)) return [];
|
|
2803
|
-
const rules = [];
|
|
2804
|
-
rules.push({
|
|
2805
|
-
name: `${spec.table}_default_admin_read`,
|
|
2806
|
-
operations: ["select"],
|
|
2807
|
-
condition: SERVER_OR_ADMIN_EXPR
|
|
2808
|
-
});
|
|
2809
|
-
rules.push({
|
|
2810
|
-
name: `${spec.table}_default_admin_write`,
|
|
2811
|
-
operations: [
|
|
2812
|
-
"insert",
|
|
2813
|
-
"update",
|
|
2814
|
-
"delete"
|
|
2815
|
-
],
|
|
2816
|
-
condition: SERVER_OR_ADMIN_EXPR,
|
|
2817
|
-
check: SERVER_OR_ADMIN_EXPR
|
|
2818
|
-
});
|
|
2819
|
-
rules.push({
|
|
2820
|
-
name: `${spec.table}_default_edge_read`,
|
|
2821
|
-
operations: ["select"],
|
|
2822
|
-
condition: policy.and(existsEndpoint(spec.endpoints[0]), existsEndpoint(spec.endpoints[1]))
|
|
2823
|
-
});
|
|
2824
|
-
const writeGrants = [];
|
|
2825
|
-
for (const side of spec.declaringSides) {
|
|
2826
|
-
const updateRules = ((isPostgresCollectionConfig(side.collection) ? side.collection.securityRules : void 0) ?? []).filter(coversUpdate);
|
|
2827
|
-
const permissive = updateRules.filter((r) => r.mode !== "restrictive");
|
|
2828
|
-
const restrictive = updateRules.filter((r) => r.mode === "restrictive");
|
|
2829
|
-
const embeddedGates = [];
|
|
2830
|
-
let gatesEmbeddable = true;
|
|
2831
|
-
for (const gate of restrictive) {
|
|
2832
|
-
const using = securityRuleToConditions(gate).usingExpr;
|
|
2833
|
-
const embedded = using ? embedParentExpression(using) : null;
|
|
2834
|
-
if (!embedded) {
|
|
2835
|
-
gatesEmbeddable = false;
|
|
2836
|
-
break;
|
|
2837
|
-
}
|
|
2838
|
-
embeddedGates.push(embedded);
|
|
2839
|
-
}
|
|
2840
|
-
if (!gatesEmbeddable) continue;
|
|
2841
|
-
const grants = [];
|
|
2842
|
-
for (const rule of permissive) {
|
|
2843
|
-
const using = securityRuleToConditions(rule).usingExpr;
|
|
2844
|
-
const embedded = using ? embedParentExpression(using) : null;
|
|
2845
|
-
if (embedded) grants.push(embedded);
|
|
2846
|
-
}
|
|
2847
|
-
if (grants.length === 0) continue;
|
|
2848
|
-
const condition = embeddedGates.length > 0 ? policy.and(policy.or(...grants), ...embeddedGates) : policy.or(...grants);
|
|
2849
|
-
writeGrants.push(existsEndpoint(side, condition));
|
|
2850
|
-
}
|
|
2851
|
-
if (writeGrants.length > 0) rules.push({
|
|
2852
|
-
name: `${spec.table}_default_edge_write`,
|
|
2853
|
-
operations: [
|
|
2854
|
-
"insert",
|
|
2855
|
-
"update",
|
|
2856
|
-
"delete"
|
|
2857
|
-
],
|
|
2858
|
-
condition: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants),
|
|
2859
|
-
check: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants)
|
|
2860
|
-
});
|
|
2861
|
-
return rules;
|
|
2862
|
-
}
|
|
2863
2287
|
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2864
2288
|
(function(root, factory) {
|
|
2865
2289
|
if (typeof define === "function" && define.amd) define(factory);
|
|
@@ -4129,45 +3553,18 @@ function deserializeFilter(query) {
|
|
|
4129
3553
|
}
|
|
4130
3554
|
//#endregion
|
|
4131
3555
|
//#region ../common/src/data/buildRebaseData.ts
|
|
4132
|
-
function createPrimaryKeyResolver(options) {
|
|
4133
|
-
const cache = /* @__PURE__ */ new Map();
|
|
4134
|
-
const warned = /* @__PURE__ */ new Set();
|
|
4135
|
-
return function primaryKeysFor(slug) {
|
|
4136
|
-
const cached = cache.get(slug);
|
|
4137
|
-
if (cached) return cached;
|
|
4138
|
-
const collection = options?.resolveCollection?.(slug);
|
|
4139
|
-
if (!collection) return [];
|
|
4140
|
-
const keys = resolvePrimaryKeys(collection);
|
|
4141
|
-
if (keys.length > 0) {
|
|
4142
|
-
cache.set(slug, keys);
|
|
4143
|
-
return keys;
|
|
4144
|
-
}
|
|
4145
|
-
if (!warned.has(slug)) {
|
|
4146
|
-
warned.add(slug);
|
|
4147
|
-
console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config — the server logs which column to mark at boot, if its schema knows the key.`);
|
|
4148
|
-
}
|
|
4149
|
-
return keys;
|
|
4150
|
-
};
|
|
4151
|
-
}
|
|
4152
3556
|
/**
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4155
|
-
* The address is *derived here* — it is not a column, and the row it came from
|
|
4156
|
-
* does not contain one. Rows carry exactly what the table has, with the types
|
|
4157
|
-
* Postgres returned; the id is this layer's invention, and this is the only
|
|
4158
|
-
* place it is minted.
|
|
4159
|
-
*
|
|
4160
|
-
* `primaryKeys` empty falls back to a literal `id` on the row: drivers other
|
|
4161
|
-
* than postgres still serve rows with one, and this keeps them working.
|
|
3557
|
+
* Convert a flat REST record (e.g. from RestFetchService) to Entity<M> format.
|
|
3558
|
+
* Mirrors the client SDK's rowToEntity conversion.
|
|
4162
3559
|
*/
|
|
4163
|
-
function rowToEntity(row, slug
|
|
3560
|
+
function rowToEntity(row, slug) {
|
|
4164
3561
|
return {
|
|
4165
|
-
id:
|
|
3562
|
+
id: row.id,
|
|
4166
3563
|
path: slug,
|
|
4167
3564
|
values: row
|
|
4168
3565
|
};
|
|
4169
3566
|
}
|
|
4170
|
-
function createDriverAccessor(driver, slug
|
|
3567
|
+
function createDriverAccessor(driver, slug) {
|
|
4171
3568
|
const accessor = {
|
|
4172
3569
|
async find(params) {
|
|
4173
3570
|
const filter = params?.where ? deserializeFilter(params.where) : void 0;
|
|
@@ -4200,7 +3597,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4200
3597
|
hasMore = offset + rows.length < total;
|
|
4201
3598
|
}
|
|
4202
3599
|
return {
|
|
4203
|
-
data: rows.map((row) => rowToEntity(row, slug
|
|
3600
|
+
data: rows.map((row) => rowToEntity(row, slug)),
|
|
4204
3601
|
meta: {
|
|
4205
3602
|
total,
|
|
4206
3603
|
limit,
|
|
@@ -4214,7 +3611,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4214
3611
|
path: slug,
|
|
4215
3612
|
id
|
|
4216
3613
|
});
|
|
4217
|
-
return row ? rowToEntity(row, slug
|
|
3614
|
+
return row ? rowToEntity(row, slug) : void 0;
|
|
4218
3615
|
},
|
|
4219
3616
|
async create(data, id) {
|
|
4220
3617
|
return rowToEntity(await driver.save({
|
|
@@ -4222,22 +3619,15 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4222
3619
|
values: data,
|
|
4223
3620
|
id,
|
|
4224
3621
|
status: "new"
|
|
4225
|
-
}), slug
|
|
3622
|
+
}), slug);
|
|
4226
3623
|
},
|
|
4227
|
-
createMany: driver.saveMany ? async (data, options) => {
|
|
4228
|
-
return (await driver.saveMany({
|
|
4229
|
-
path: slug,
|
|
4230
|
-
rows: data,
|
|
4231
|
-
upsert: options?.upsert
|
|
4232
|
-
})).map((row) => rowToEntity(row, slug, getPks()));
|
|
4233
|
-
} : void 0,
|
|
4234
3624
|
async update(id, data) {
|
|
4235
3625
|
return rowToEntity(await driver.save({
|
|
4236
3626
|
path: slug,
|
|
4237
3627
|
values: data,
|
|
4238
3628
|
id,
|
|
4239
3629
|
status: "existing"
|
|
4240
|
-
}), slug
|
|
3630
|
+
}), slug);
|
|
4241
3631
|
},
|
|
4242
3632
|
async delete(id) {
|
|
4243
3633
|
return driver.delete({ row: {
|
|
@@ -4266,7 +3656,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4266
3656
|
searchString: params?.searchString,
|
|
4267
3657
|
onUpdate: (entities) => {
|
|
4268
3658
|
onUpdate({
|
|
4269
|
-
data: entities.map((row) => rowToEntity(row, slug
|
|
3659
|
+
data: entities.map((row) => rowToEntity(row, slug)),
|
|
4270
3660
|
meta: {
|
|
4271
3661
|
total: entities.length,
|
|
4272
3662
|
limit,
|
|
@@ -4282,7 +3672,7 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4282
3672
|
return driver.listenOne({
|
|
4283
3673
|
path: slug,
|
|
4284
3674
|
id,
|
|
4285
|
-
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug
|
|
3675
|
+
onUpdate: (entity) => onUpdate(entity ? rowToEntity(entity, slug) : void 0),
|
|
4286
3676
|
onError
|
|
4287
3677
|
});
|
|
4288
3678
|
} : void 0,
|
|
@@ -4321,13 +3711,12 @@ function createDriverAccessor(driver, slug, getPks = () => []) {
|
|
|
4321
3711
|
* await data.products.create({ name: "Camera", price: 299 });
|
|
4322
3712
|
* const { data: items } = await data.products.find({ where: { status: ["==", "published"] } });
|
|
4323
3713
|
*/
|
|
4324
|
-
function buildRebaseData(driver
|
|
3714
|
+
function buildRebaseData(driver) {
|
|
4325
3715
|
const cache = /* @__PURE__ */ new Map();
|
|
4326
|
-
const primaryKeysFor = createPrimaryKeyResolver(options);
|
|
4327
3716
|
function getAccessor(slug) {
|
|
4328
3717
|
let accessor = cache.get(slug);
|
|
4329
3718
|
if (!accessor) {
|
|
4330
|
-
accessor = createDriverAccessor(driver, slug
|
|
3719
|
+
accessor = createDriverAccessor(driver, slug);
|
|
4331
3720
|
cache.set(slug, accessor);
|
|
4332
3721
|
}
|
|
4333
3722
|
return accessor;
|
|
@@ -4340,9 +3729,8 @@ function buildRebaseData(driver, options) {
|
|
|
4340
3729
|
} });
|
|
4341
3730
|
}
|
|
4342
3731
|
/**
|
|
4343
|
-
* Unwrap a Entity
|
|
4344
|
-
*
|
|
4345
|
-
* the wrapper is the whole operation — the address was never part of the row.
|
|
3732
|
+
* Unwrap a Entity into a flat row. `rowToEntity` stores the whole flat row
|
|
3733
|
+
* (id included) under `.values`, so this is just that payload.
|
|
4346
3734
|
*/
|
|
4347
3735
|
function entityToRow(entity) {
|
|
4348
3736
|
return entity.values;
|
|
@@ -4429,12 +3817,6 @@ function toSdkCollectionClient(snap) {
|
|
|
4429
3817
|
async create(data, id) {
|
|
4430
3818
|
return entityToRow(await snap.create(data, id));
|
|
4431
3819
|
},
|
|
4432
|
-
async createMany(data, options) {
|
|
4433
|
-
if (!Array.isArray(data)) throw new TypeError("createMany expects an array of records.");
|
|
4434
|
-
if (data.length === 0) return [];
|
|
4435
|
-
if (!snap.createMany) throw new Error("Bulk writes are not supported by this collection's data source. Fall back to create() per record.");
|
|
4436
|
-
return (await snap.createMany(data, options)).map(entityToRow);
|
|
4437
|
-
},
|
|
4438
3820
|
async update(id, data) {
|
|
4439
3821
|
return entityToRow(await snap.update(id, data));
|
|
4440
3822
|
},
|
|
@@ -4587,26 +3969,8 @@ function getTableForCollection(collection, registry) {
|
|
|
4587
3969
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4588
3970
|
return table;
|
|
4589
3971
|
}
|
|
4590
|
-
/**
|
|
4591
|
-
* The key columns a collection's rows are addressed by.
|
|
4592
|
-
*
|
|
4593
|
-
* Three tiers, in order: properties marked `isId`, the primary keys of the
|
|
4594
|
-
* drizzle schema, and finally a column literally named `id`. Only the first is
|
|
4595
|
-
* visible to the browser, which is why a key known only to drizzle is reported
|
|
4596
|
-
* at boot — see {@link warnOnKeysTheAdminCannotResolve}.
|
|
4597
|
-
*
|
|
4598
|
-
* Returns `[]` when nothing resolves, rather than throwing. It used to open by
|
|
4599
|
-
* resolving the table, which throws when there is none — so the `isId` tier,
|
|
4600
|
-
* which needs no table at all, was unreachable for exactly the collections
|
|
4601
|
-
* most likely to have no table registered. Every caller that wanted "no keys"
|
|
4602
|
-
* to mean "no keys" had to spell that out in a try/catch.
|
|
4603
|
-
*
|
|
4604
|
-
* Callers that cannot proceed without a key must say so themselves, naming the
|
|
4605
|
-
* collection: an empty array here means "this collection has no address", which
|
|
4606
|
-
* is a different answer in a notification (broadcast a wildcard) than in a save
|
|
4607
|
-
* (fail).
|
|
4608
|
-
*/
|
|
4609
3972
|
function getPrimaryKeys(collection, registry) {
|
|
3973
|
+
const table = getTableForCollection(collection, registry);
|
|
4610
3974
|
if (collection.properties) {
|
|
4611
3975
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4612
3976
|
fieldName: key,
|
|
@@ -4615,8 +3979,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4615
3979
|
}));
|
|
4616
3980
|
if (idProps.length > 0) return idProps;
|
|
4617
3981
|
}
|
|
4618
|
-
const table = registry.getTable(getTableName$1(collection));
|
|
4619
|
-
if (!table) return [];
|
|
4620
3982
|
const keys = [];
|
|
4621
3983
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4622
3984
|
const col = colRaw;
|
|
@@ -4644,89 +4006,35 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4644
4006
|
}
|
|
4645
4007
|
return keys;
|
|
4646
4008
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
const keys = getPrimaryKeys(collection, registry);
|
|
4659
|
-
if (keys.length === 0) throw new Error(`Collection '${collection.slug}' has no primary key, so its rows cannot be addressed. Mark the key property with \`isId\` in its config, or register a table whose schema declares one.`);
|
|
4660
|
-
return keys;
|
|
4661
|
-
}
|
|
4662
|
-
/**
|
|
4663
|
-
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
4664
|
-
* instead.
|
|
4665
|
-
*
|
|
4666
|
-
* The two sides resolve keys from different evidence. This driver reads, in
|
|
4667
|
-
* order: properties marked `isId`, the primary keys of the Drizzle schema, then
|
|
4668
|
-
* a column literally named `id`. The admin shares the `CollectionConfig` — it
|
|
4669
|
-
* compiles the same collection files into its bundle — but never the Drizzle
|
|
4670
|
-
* schema, so the middle tier is invisible to it.
|
|
4671
|
-
*
|
|
4672
|
-
* Nothing can normalize this at runtime: the server does not serve the admin
|
|
4673
|
-
* its collections, so a key resolved here cannot be handed over there. The
|
|
4674
|
-
* config files are the only thing both sides read, so the fix is an edit to
|
|
4675
|
-
* them, and the most this can do is say exactly which edit.
|
|
4676
|
-
*
|
|
4677
|
-
* Two shapes, and the second is the dangerous one:
|
|
4678
|
-
*
|
|
4679
|
-
* - No `isId`, no `id` property → the admin resolves no address, warns in the
|
|
4680
|
-
* console, and rows cannot be opened or linked.
|
|
4681
|
-
* - No `isId`, but an `id` property that is *not* the key → the admin addresses
|
|
4682
|
-
* rows by `id` while this driver reads the address as the real key. Nothing
|
|
4683
|
-
* errors: the addresses look right and route wrong.
|
|
4684
|
-
*/
|
|
4685
|
-
function findUnresolvableKeyCollections(collections, registry) {
|
|
4686
|
-
const findings = [];
|
|
4687
|
-
for (const collection of collections) {
|
|
4688
|
-
if (getDeclaredPrimaryKeys(collection).length > 0) continue;
|
|
4689
|
-
const keys = getPrimaryKeys(collection, registry);
|
|
4690
|
-
if (keys.length === 0) continue;
|
|
4691
|
-
if (keys.length === 1 && keys[0].fieldName === "id") continue;
|
|
4692
|
-
findings.push({
|
|
4693
|
-
collection,
|
|
4694
|
-
keys,
|
|
4695
|
-
shadowedByIdProperty: Boolean(collection.properties?.id)
|
|
4696
|
-
});
|
|
4009
|
+
function parseIdValues(idValue, primaryKeys) {
|
|
4010
|
+
const result = {};
|
|
4011
|
+
if (primaryKeys.length === 0) return result;
|
|
4012
|
+
if (primaryKeys.length === 1) {
|
|
4013
|
+
const pk = primaryKeys[0];
|
|
4014
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
4015
|
+
const parsed = typeof idValue === "number" ? idValue : parseInt(String(idValue), 10);
|
|
4016
|
+
if (isNaN(parsed)) throw new Error(`Invalid numeric ID: ${idValue}`);
|
|
4017
|
+
result[pk.fieldName] = parsed;
|
|
4018
|
+
} else result[pk.fieldName] = String(idValue);
|
|
4019
|
+
return result;
|
|
4697
4020
|
}
|
|
4698
|
-
|
|
4699
|
-
}
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
const edit = (f) => `${f.collection.slug}: mark ${f.keys.map((k) => `\`${k.fieldName}\``).join(" and ")} with \`isId: ${f.keys[0].isUUID ? "\"uuid\"" : f.keys[0].type === "number" ? "\"increment\"" : "true"}\``;
|
|
4711
|
-
const shadowed = findings.filter((f) => f.shadowedByIdProperty);
|
|
4712
|
-
const silent = findings.filter((f) => !f.shadowedByIdProperty);
|
|
4713
|
-
if (shadowed.length > 0) logger.warn("⚠️ These collections declare no `isId`, and their key is only in the drizzle schema — but they do have a property called `id`. The admin has no way to know `id` is not the key, so it will address rows by it while this server reads the address as the real key: the links look right and route wrong. Nothing will error.\n\n" + shadowed.map((f) => ` • ${edit(f)}`).join("\n") + "\n");
|
|
4714
|
-
if (silent.length > 0) logger.warn("⚠️ These collections declare no `isId`, and their key is only in the drizzle schema, which the admin never sees. It will resolve no address for their rows, so detail links, caching and relations will not work for them:\n\n" + silent.map((f) => ` • ${edit(f)}`).join("\n") + "\n");
|
|
4021
|
+
const parts = String(idValue).split(":::");
|
|
4022
|
+
if (parts.length !== primaryKeys.length) throw new Error(`Composite ID parts mismatch. Expected ${primaryKeys.length}, got ${parts.length} for ID: ${idValue}`);
|
|
4023
|
+
for (let i = 0; i < primaryKeys.length; i++) {
|
|
4024
|
+
const pk = primaryKeys[i];
|
|
4025
|
+
const val = parts[i];
|
|
4026
|
+
if (pk.type === "number" && !pk.isUUID) {
|
|
4027
|
+
const parsed = parseInt(val, 10);
|
|
4028
|
+
if (isNaN(parsed)) throw new Error(`Invalid numeric ID component: ${val}`);
|
|
4029
|
+
result[pk.fieldName] = parsed;
|
|
4030
|
+
} else result[pk.fieldName] = val;
|
|
4031
|
+
}
|
|
4032
|
+
return result;
|
|
4715
4033
|
}
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
* Falls back to a literal `id` column, for a row that reached us from somewhere
|
|
4721
|
-
* other than this driver. Returns `""` when there is no key and no `id` —
|
|
4722
|
-
* callers decide what that means, since "unaddressable" is a different answer
|
|
4723
|
-
* in a notification (broadcast a wildcard) than in a save (fail).
|
|
4724
|
-
*/
|
|
4725
|
-
function deriveRowAddress(row, collection, registry) {
|
|
4726
|
-
const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
|
|
4727
|
-
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4728
|
-
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
4729
|
-
return "";
|
|
4034
|
+
function buildCompositeId(values, primaryKeys) {
|
|
4035
|
+
if (primaryKeys.length === 0) return "";
|
|
4036
|
+
if (primaryKeys.length === 1) return String(values[primaryKeys[0].fieldName] ?? "");
|
|
4037
|
+
return primaryKeys.map((pk) => String(values[pk.fieldName] ?? "")).join(":::");
|
|
4730
4038
|
}
|
|
4731
4039
|
//#endregion
|
|
4732
4040
|
//#region src/utils/drizzle-conditions.ts
|
|
@@ -5266,7 +4574,6 @@ var DrizzleConditionBuilder = class {
|
|
|
5266
4574
|
static buildVectorSearchConditions(table, vectorSearch) {
|
|
5267
4575
|
const column = table[vectorSearch.property];
|
|
5268
4576
|
if (!column) throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
5269
|
-
if (!Array.isArray(vectorSearch.vector) || vectorSearch.vector.length === 0 || !vectorSearch.vector.every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error("Vector search requires a non-empty array of finite numbers");
|
|
5270
4577
|
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
5271
4578
|
const distanceFn = vectorSearch.distance || "cosine";
|
|
5272
4579
|
let operator;
|
|
@@ -5350,10 +4657,12 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5350
4657
|
continue;
|
|
5351
4658
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
5352
4659
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4660
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
5353
4661
|
inverseRelationUpdates.push({
|
|
5354
4662
|
relationKey: key,
|
|
5355
4663
|
relation,
|
|
5356
|
-
newValue: serializedValue
|
|
4664
|
+
newValue: serializedValue,
|
|
4665
|
+
currentId: row.id || buildCompositeId(row, pks)
|
|
5357
4666
|
});
|
|
5358
4667
|
continue;
|
|
5359
4668
|
} else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
@@ -5363,11 +4672,15 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5363
4672
|
relation,
|
|
5364
4673
|
newTargetId: serializedValue
|
|
5365
4674
|
});
|
|
5366
|
-
else
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
4675
|
+
else {
|
|
4676
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
4677
|
+
inverseRelationUpdates.push({
|
|
4678
|
+
relationKey: key,
|
|
4679
|
+
relation,
|
|
4680
|
+
newValue: serializedValue,
|
|
4681
|
+
currentId: row.id || buildCompositeId(row, pks)
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
5371
4684
|
continue;
|
|
5372
4685
|
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
5373
4686
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
@@ -5767,63 +5080,6 @@ var RelationService = class {
|
|
|
5767
5080
|
this.registry = registry;
|
|
5768
5081
|
}
|
|
5769
5082
|
/**
|
|
5770
|
-
* One target row, as the {@link RelatedRow} everything here returns.
|
|
5771
|
-
*
|
|
5772
|
-
* Eight sites built this by hand, which is how the address came to be the
|
|
5773
|
-
* target's first key column in all eight — one edit, eight places to miss.
|
|
5774
|
-
*
|
|
5775
|
-
* `resolveNested` is the one thing they did not agree on, and the
|
|
5776
|
-
* disagreement was invisible: the single-parent fetches pass `db` and
|
|
5777
|
-
* `registry` to `parseDataFromServer`, so the target's *own* relations get
|
|
5778
|
-
* resolved too, while the batch paths deliberately do not — a query per
|
|
5779
|
-
* target row is the N+1 the batching exists to avoid. Naming the parameter
|
|
5780
|
-
* makes that a decision rather than a difference between two call sites
|
|
5781
|
-
* nobody was comparing.
|
|
5782
|
-
*/
|
|
5783
|
-
async toRelatedRow(targetRow, targetCollection, targetPks, options) {
|
|
5784
|
-
const values = options?.resolveNested ? await parseDataFromServer(targetRow, targetCollection, this.db, this.registry) : await parseDataFromServer(targetRow, targetCollection);
|
|
5785
|
-
return {
|
|
5786
|
-
id: buildCompositeId(targetRow, targetPks),
|
|
5787
|
-
path: targetCollection.slug,
|
|
5788
|
-
values
|
|
5789
|
-
};
|
|
5790
|
-
}
|
|
5791
|
-
/**
|
|
5792
|
-
* A WHERE matching any of `parentIds`, by the whole key.
|
|
5793
|
-
*
|
|
5794
|
-
* A single key is an `IN (…)`. A composite one cannot be: matching
|
|
5795
|
-
* `tenant_id IN (1, 1)` collects every row of tenant 1, so two parents that
|
|
5796
|
-
* share their first column each receive the other's relations. It becomes
|
|
5797
|
-
* an OR of ANDs — one exact address per parent — which Postgres indexes the
|
|
5798
|
-
* same way it would a multi-column key lookup.
|
|
5799
|
-
*/
|
|
5800
|
-
parentKeyCondition(parentTable, parentPks, parentIds) {
|
|
5801
|
-
const columnFor = (fieldName) => {
|
|
5802
|
-
const col = parentTable[fieldName];
|
|
5803
|
-
if (!col) throw new Error(`Key column '${fieldName}' not found in parent table`);
|
|
5804
|
-
return col;
|
|
5805
|
-
};
|
|
5806
|
-
if (parentPks.length === 1) {
|
|
5807
|
-
const values = parentIds.map((id) => parseIdValues(id, parentPks)[parentPks[0].fieldName]);
|
|
5808
|
-
return inArray(columnFor(parentPks[0].fieldName), values);
|
|
5809
|
-
}
|
|
5810
|
-
return or(...parentIds.map((id) => {
|
|
5811
|
-
const values = parseIdValues(id, parentPks);
|
|
5812
|
-
return and(...parentPks.map((pk) => eq(columnFor(pk.fieldName), values[pk.fieldName])));
|
|
5813
|
-
}));
|
|
5814
|
-
}
|
|
5815
|
-
/**
|
|
5816
|
-
* Reject a relation that cannot express a composite-keyed parent.
|
|
5817
|
-
*
|
|
5818
|
-
* `localKey` and `foreignKeyOnTarget` are single column names: one column
|
|
5819
|
-
* cannot reference a two-column key, so such a relation has no correct
|
|
5820
|
-
* reading. Left alone it would silently match on the first key column and
|
|
5821
|
-
* hand a tenant's rows to its neighbour — say so instead.
|
|
5822
|
-
*/
|
|
5823
|
-
assertSingleKeyAddressable(parentCollection, parentPks, via) {
|
|
5824
|
-
if (parentPks.length > 1) throw new Error(`Relation on '${parentCollection.slug}' uses '${via}', a single foreign-key column, but '${parentCollection.slug}' is keyed on ${parentPks.map((k) => `'${k.fieldName}'`).join(" + ")}. One column cannot reference a composite key — express this relation with \`joinPath\`, whose \`on.from\`/\`on.to\` take every key column.`);
|
|
5825
|
-
}
|
|
5826
|
-
/**
|
|
5827
5083
|
* Fetch rows related to a parent row through a specific relation
|
|
5828
5084
|
*/
|
|
5829
5085
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5842,9 +5098,9 @@ var RelationService = class {
|
|
|
5842
5098
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5843
5099
|
const targetCollection = relation.target();
|
|
5844
5100
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5845
|
-
const idInfo =
|
|
5101
|
+
const idInfo = getPrimaryKeys(targetCollection, this.registry);
|
|
5846
5102
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5847
|
-
const parentPks =
|
|
5103
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5848
5104
|
const parentIdInfo = parentPks[0];
|
|
5849
5105
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5850
5106
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5868,7 +5124,7 @@ var RelationService = class {
|
|
|
5868
5124
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5869
5125
|
currentTable = joinTable;
|
|
5870
5126
|
}
|
|
5871
|
-
const parentIdField = parentTable[
|
|
5127
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5872
5128
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5873
5129
|
if (options.limit) query = query.limit(options.limit);
|
|
5874
5130
|
const results = await query;
|
|
@@ -5876,7 +5132,13 @@ var RelationService = class {
|
|
|
5876
5132
|
const rows = [];
|
|
5877
5133
|
for (const row of results) {
|
|
5878
5134
|
const targetRow = row[targetTableName] || row;
|
|
5879
|
-
|
|
5135
|
+
const id = targetRow[idInfo[0].fieldName];
|
|
5136
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5137
|
+
rows.push({
|
|
5138
|
+
id: id?.toString() || "",
|
|
5139
|
+
path: targetCollection.slug,
|
|
5140
|
+
values: parsedValues
|
|
5141
|
+
});
|
|
5880
5142
|
}
|
|
5881
5143
|
return rows;
|
|
5882
5144
|
}
|
|
@@ -5895,7 +5157,13 @@ var RelationService = class {
|
|
|
5895
5157
|
const rows = [];
|
|
5896
5158
|
for (const row of results) {
|
|
5897
5159
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5898
|
-
|
|
5160
|
+
const id = targetRow[idInfo[0].fieldName];
|
|
5161
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5162
|
+
rows.push({
|
|
5163
|
+
id: id?.toString() || "",
|
|
5164
|
+
path: targetCollection.slug,
|
|
5165
|
+
values: parsedValues
|
|
5166
|
+
});
|
|
5899
5167
|
}
|
|
5900
5168
|
return rows;
|
|
5901
5169
|
}
|
|
@@ -5912,8 +5180,8 @@ var RelationService = class {
|
|
|
5912
5180
|
}
|
|
5913
5181
|
const targetCollection = relation.target();
|
|
5914
5182
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5915
|
-
const targetIdField = targetTable[
|
|
5916
|
-
const parentPks =
|
|
5183
|
+
const targetIdField = targetTable[getPrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5184
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5917
5185
|
const parentIdInfo = parentPks[0];
|
|
5918
5186
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5919
5187
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5932,10 +5200,9 @@ var RelationService = class {
|
|
|
5932
5200
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5933
5201
|
const targetCollection = relation.target();
|
|
5934
5202
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5935
|
-
const
|
|
5936
|
-
const targetIdInfo = targetPks[0];
|
|
5203
|
+
const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
|
|
5937
5204
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5938
|
-
const parentPks =
|
|
5205
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5939
5206
|
const parentIdInfo = parentPks[0];
|
|
5940
5207
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5941
5208
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5959,19 +5226,25 @@ var RelationService = class {
|
|
|
5959
5226
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5960
5227
|
currentTable = joinTable;
|
|
5961
5228
|
}
|
|
5962
|
-
|
|
5229
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5230
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5963
5231
|
const results = await query;
|
|
5964
5232
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5965
5233
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5966
5234
|
for (const row of results) {
|
|
5967
5235
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5968
5236
|
const targetRow = row[targetTableName] || row;
|
|
5969
|
-
|
|
5237
|
+
const parentId = parentRow[parentIdInfo.fieldName];
|
|
5238
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5239
|
+
resultMap.set(String(parentId), {
|
|
5240
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5241
|
+
path: targetCollection.slug,
|
|
5242
|
+
values: parsedValues
|
|
5243
|
+
});
|
|
5970
5244
|
}
|
|
5971
5245
|
return resultMap;
|
|
5972
5246
|
}
|
|
5973
5247
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5974
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5975
5248
|
const localKeyCol = parentTable[relation.localKey];
|
|
5976
5249
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5977
5250
|
const fkRows = await this.db.select({
|
|
@@ -6000,11 +5273,17 @@ var RelationService = class {
|
|
|
6000
5273
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6001
5274
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
6002
5275
|
const targetRow = targetById.get(String(fkValue));
|
|
6003
|
-
if (targetRow)
|
|
5276
|
+
if (targetRow) {
|
|
5277
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5278
|
+
resultMap.set(parentIdStr, {
|
|
5279
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5280
|
+
path: targetCollection.slug,
|
|
5281
|
+
values: parsedValues
|
|
5282
|
+
});
|
|
5283
|
+
}
|
|
6004
5284
|
}
|
|
6005
5285
|
return resultMap;
|
|
6006
5286
|
}
|
|
6007
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6008
5287
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6009
5288
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6010
5289
|
const results = await query;
|
|
@@ -6015,7 +5294,14 @@ var RelationService = class {
|
|
|
6015
5294
|
let parentId;
|
|
6016
5295
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6017
5296
|
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6018
|
-
if (parentId !== void 0 && parentIdSet.has(String(parentId)))
|
|
5297
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5298
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5299
|
+
resultMap.set(String(parentId), {
|
|
5300
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5301
|
+
path: targetCollection.slug,
|
|
5302
|
+
values: parsedValues
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
6019
5305
|
}
|
|
6020
5306
|
return resultMap;
|
|
6021
5307
|
}
|
|
@@ -6029,9 +5315,9 @@ var RelationService = class {
|
|
|
6029
5315
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
6030
5316
|
const targetCollection = relation.target();
|
|
6031
5317
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6032
|
-
const
|
|
6033
|
-
const targetIdField = targetTable[
|
|
6034
|
-
const parentPks =
|
|
5318
|
+
const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
|
|
5319
|
+
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5320
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6035
5321
|
const parentIdInfo = parentPks[0];
|
|
6036
5322
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
6037
5323
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -6053,22 +5339,27 @@ var RelationService = class {
|
|
|
6053
5339
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
6054
5340
|
currentTable = joinTable;
|
|
6055
5341
|
}
|
|
6056
|
-
|
|
5342
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5343
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6057
5344
|
const results = await query;
|
|
6058
5345
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
6059
5346
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6060
5347
|
for (const row of results) {
|
|
6061
5348
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
6062
5349
|
const targetRow = row[targetTableName] || row;
|
|
6063
|
-
const parentId =
|
|
5350
|
+
const parentId = String(parentRow[parentIdInfo.fieldName]);
|
|
5351
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6064
5352
|
const arr = resultMap.get(parentId) || [];
|
|
6065
|
-
arr.push(
|
|
5353
|
+
arr.push({
|
|
5354
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5355
|
+
path: targetCollection.slug,
|
|
5356
|
+
values: parsedValues
|
|
5357
|
+
});
|
|
6066
5358
|
resultMap.set(parentId, arr);
|
|
6067
5359
|
}
|
|
6068
5360
|
return resultMap;
|
|
6069
5361
|
}
|
|
6070
5362
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6071
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
6072
5363
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
6073
5364
|
if (!junctionTable) {
|
|
6074
5365
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -6087,13 +5378,17 @@ var RelationService = class {
|
|
|
6087
5378
|
const junctionData = row[relation.through.table] || row;
|
|
6088
5379
|
const targetData = row[targetTableName] || row;
|
|
6089
5380
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
5381
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
6090
5382
|
const arr = resultMap.get(parentId) || [];
|
|
6091
|
-
arr.push(
|
|
5383
|
+
arr.push({
|
|
5384
|
+
id: String(targetData[targetIdInfo.fieldName]),
|
|
5385
|
+
path: targetCollection.slug,
|
|
5386
|
+
values: parsedValues
|
|
5387
|
+
});
|
|
6092
5388
|
resultMap.set(parentId, arr);
|
|
6093
5389
|
}
|
|
6094
5390
|
return resultMap;
|
|
6095
5391
|
}
|
|
6096
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6097
5392
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6098
5393
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6099
5394
|
const results = await query;
|
|
@@ -6106,9 +5401,14 @@ var RelationService = class {
|
|
|
6106
5401
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6107
5402
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6108
5403
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5404
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6109
5405
|
const key = String(parentId);
|
|
6110
5406
|
const arr = resultMap.get(key) || [];
|
|
6111
|
-
arr.push(
|
|
5407
|
+
arr.push({
|
|
5408
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5409
|
+
path: targetCollection.slug,
|
|
5410
|
+
values: parsedValues
|
|
5411
|
+
});
|
|
6112
5412
|
resultMap.set(key, arr);
|
|
6113
5413
|
}
|
|
6114
5414
|
}
|
|
@@ -6156,12 +5456,12 @@ var RelationService = class {
|
|
|
6156
5456
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
6157
5457
|
continue;
|
|
6158
5458
|
}
|
|
6159
|
-
const parentPks =
|
|
5459
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6160
5460
|
const parentIdInfo = parentPks[0];
|
|
6161
5461
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6162
5462
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6163
5463
|
if (targetEntityIds.length > 0) {
|
|
6164
|
-
const targetPks =
|
|
5464
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6165
5465
|
const targetIdInfo = targetPks[0];
|
|
6166
5466
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6167
5467
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6181,12 +5481,12 @@ var RelationService = class {
|
|
|
6181
5481
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6182
5482
|
continue;
|
|
6183
5483
|
}
|
|
6184
|
-
const parentPks =
|
|
5484
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6185
5485
|
const parentIdInfo = parentPks[0];
|
|
6186
5486
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6187
5487
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6188
5488
|
if (targetEntityIds.length > 0) {
|
|
6189
|
-
const targetPks =
|
|
5489
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6190
5490
|
const targetIdInfo = targetPks[0];
|
|
6191
5491
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6192
5492
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6197,7 +5497,7 @@ var RelationService = class {
|
|
|
6197
5497
|
} else if (relation.through && relation.cardinality === "many" && relation.direction === "inverse") logger.warn(`[updateRelationsUsingJoins] Inverse M2M relation '${key}' in collection '${collection.slug}' should be saved from the owning side. Skipping.`);
|
|
6198
5498
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6199
5499
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6200
|
-
const targetPks =
|
|
5500
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6201
5501
|
const targetIdInfo = targetPks[0];
|
|
6202
5502
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6203
5503
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6205,7 +5505,7 @@ var RelationService = class {
|
|
|
6205
5505
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6206
5506
|
continue;
|
|
6207
5507
|
}
|
|
6208
|
-
const parentPks =
|
|
5508
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6209
5509
|
const parentIdInfo = parentPks[0];
|
|
6210
5510
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6211
5511
|
if (targetEntityIds.length > 0) {
|
|
@@ -6225,9 +5525,9 @@ var RelationService = class {
|
|
|
6225
5525
|
try {
|
|
6226
5526
|
const targetCollection = relation.target();
|
|
6227
5527
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6228
|
-
const targetPks =
|
|
5528
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6229
5529
|
const targetIdInfo = targetPks[0];
|
|
6230
|
-
const sourcePks =
|
|
5530
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6231
5531
|
const sourceIdInfo = sourcePks[0];
|
|
6232
5532
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6233
5533
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6308,12 +5608,12 @@ var RelationService = class {
|
|
|
6308
5608
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6309
5609
|
return;
|
|
6310
5610
|
}
|
|
6311
|
-
const sourcePks =
|
|
5611
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6312
5612
|
const sourceIdInfo = sourcePks[0];
|
|
6313
5613
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6314
5614
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6315
5615
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6316
|
-
const targetPks =
|
|
5616
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6317
5617
|
const targetIdInfo = targetPks[0];
|
|
6318
5618
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6319
5619
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6321,7 +5621,7 @@ var RelationService = class {
|
|
|
6321
5621
|
}));
|
|
6322
5622
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6323
5623
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6324
|
-
const targetPks =
|
|
5624
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6325
5625
|
const targetIdInfo = targetPks[0];
|
|
6326
5626
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6327
5627
|
const newLink = {
|
|
@@ -6352,12 +5652,12 @@ var RelationService = class {
|
|
|
6352
5652
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6353
5653
|
return;
|
|
6354
5654
|
}
|
|
6355
|
-
const sourcePks =
|
|
5655
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6356
5656
|
const sourceIdInfo = sourcePks[0];
|
|
6357
5657
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6358
5658
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6359
5659
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6360
|
-
const targetPks =
|
|
5660
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6361
5661
|
const targetIdInfo = targetPks[0];
|
|
6362
5662
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6363
5663
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6378,12 +5678,12 @@ var RelationService = class {
|
|
|
6378
5678
|
const { relation, newTargetId } = upd;
|
|
6379
5679
|
const targetCollection = relation.target();
|
|
6380
5680
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6381
|
-
const targetPks =
|
|
5681
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6382
5682
|
const targetIdInfo = targetPks[0];
|
|
6383
5683
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6384
5684
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6385
5685
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6386
|
-
const parentPks =
|
|
5686
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6387
5687
|
const parentIdInfo = parentPks[0];
|
|
6388
5688
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6389
5689
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6454,7 +5754,7 @@ var RelationService = class {
|
|
|
6454
5754
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6455
5755
|
return;
|
|
6456
5756
|
}
|
|
6457
|
-
const targetPks =
|
|
5757
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6458
5758
|
const targetIdInfo = targetPks[0];
|
|
6459
5759
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6460
5760
|
const junctionData = {
|
|
@@ -6470,131 +5770,6 @@ var RelationService = class {
|
|
|
6470
5770
|
}
|
|
6471
5771
|
};
|
|
6472
5772
|
//#endregion
|
|
6473
|
-
//#region src/services/row-pipeline.ts
|
|
6474
|
-
/**
|
|
6475
|
-
* Whether a many-relation reaches its target through a junction table.
|
|
6476
|
-
*
|
|
6477
|
-
* Also used to build the drizzle `with` config, which is why it is exported:
|
|
6478
|
-
* the query has to nest one level deeper for a junction, and the row walk has
|
|
6479
|
-
* to unwrap that same level back out.
|
|
6480
|
-
*/
|
|
6481
|
-
function isJunctionRelation(relation) {
|
|
6482
|
-
if (relation.through) return true;
|
|
6483
|
-
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6484
|
-
return false;
|
|
6485
|
-
}
|
|
6486
|
-
/**
|
|
6487
|
-
* Reach the target row inside a junction row.
|
|
6488
|
-
*
|
|
6489
|
-
* A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
|
|
6490
|
-
* the foreign keys, and the target nested under one of them. The target is the
|
|
6491
|
-
* only object among them, so that is how it is found. A junction row that has
|
|
6492
|
-
* not been nested (no `with` on the join) has no object and is returned as-is.
|
|
6493
|
-
*/
|
|
6494
|
-
function unwrapJunctionRow(item) {
|
|
6495
|
-
const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
|
|
6496
|
-
return nestedKey ? item[nestedKey] : item;
|
|
6497
|
-
}
|
|
6498
|
-
/**
|
|
6499
|
-
* Give back the number a `number` property was declared to be.
|
|
6500
|
-
*
|
|
6501
|
-
* Postgres returns NUMERIC as a string and drizzle keeps it that way, since a
|
|
6502
|
-
* numeric can hold more precision than a double. But the collection declared
|
|
6503
|
-
* this property `number` and the OpenAPI spec this server publishes says
|
|
6504
|
-
* `type: number`, so serving `"9.99"` breaks its own contract — and breaks it
|
|
6505
|
-
* asymmetrically, because a create answers with the number and the read that
|
|
6506
|
-
* follows answers with the string. Any client that multiplies a price works
|
|
6507
|
-
* until the first refresh.
|
|
6508
|
-
*
|
|
6509
|
-
* Declaring a property `number` already accepts double precision — that is what
|
|
6510
|
-
* the admin has always parsed it to. Columns REST serves that no property
|
|
6511
|
-
* declares are left exactly as the database returned them.
|
|
6512
|
-
*/
|
|
6513
|
-
function coerceDeclaredNumber(value, property) {
|
|
6514
|
-
if (property?.type !== "number" || typeof value !== "string") return value;
|
|
6515
|
-
const parsed = parseFloat(value);
|
|
6516
|
-
return isNaN(parsed) ? null : parsed;
|
|
6517
|
-
}
|
|
6518
|
-
/** Apply {@link coerceDeclaredNumber} across a row, leaving every other column alone. */
|
|
6519
|
-
function coerceDeclaredNumbers(row, collection) {
|
|
6520
|
-
const properties = collection.properties;
|
|
6521
|
-
if (!properties) return row;
|
|
6522
|
-
const out = {};
|
|
6523
|
-
for (const [key, value] of Object.entries(row)) out[key] = coerceDeclaredNumber(value, properties[key]);
|
|
6524
|
-
return out;
|
|
6525
|
-
}
|
|
6526
|
-
/** Render one target row in the requested style. */
|
|
6527
|
-
function renderTarget(targetRow, targetCollection, style, registry) {
|
|
6528
|
-
if (style === "inline") return coerceDeclaredNumbers({ ...targetRow }, targetCollection);
|
|
6529
|
-
const address = relationTargetAddress(targetRow, targetCollection, registry);
|
|
6530
|
-
const path = targetCollection.slug;
|
|
6531
|
-
return createRelationRefWithData(address, path, {
|
|
6532
|
-
id: address,
|
|
6533
|
-
path,
|
|
6534
|
-
values: normalizeDbValues(targetRow, targetCollection)
|
|
6535
|
-
});
|
|
6536
|
-
}
|
|
6537
|
-
/**
|
|
6538
|
-
* The address a relation ref points at.
|
|
6539
|
-
*
|
|
6540
|
-
* The whole key, not its first column: a composite-keyed target addressed by
|
|
6541
|
-
* `tenant_id` alone points at every row that shares it. A target whose key
|
|
6542
|
-
* cannot be resolved at all used to throw here — reading `[0]` of an empty
|
|
6543
|
-
* array — taking down the parent's fetch over a relation it may not even have
|
|
6544
|
-
* asked for. The first column is a guess, but a ref that resolves to nothing
|
|
6545
|
-
* beats no rows at all.
|
|
6546
|
-
*/
|
|
6547
|
-
function relationTargetAddress(targetRow, targetCollection, registry) {
|
|
6548
|
-
const address = deriveRowAddress(targetRow, targetCollection, registry);
|
|
6549
|
-
if (address) return address;
|
|
6550
|
-
return String(targetRow[Object.keys(targetRow)[0]] ?? "");
|
|
6551
|
-
}
|
|
6552
|
-
/**
|
|
6553
|
-
* The row the admin renders: every column, with relations as references.
|
|
6554
|
-
*
|
|
6555
|
-
* Values are normalized (dates, numbers, NaN) because the admin's view-model
|
|
6556
|
-
* expects real types. The row's own address is *not* among the columns — it is
|
|
6557
|
-
* derived by the consumer from the collection's primary keys.
|
|
6558
|
-
*/
|
|
6559
|
-
function toCmsRow(row, collection, registry) {
|
|
6560
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6561
|
-
const normalized = normalizeDbValues(row, collection);
|
|
6562
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6563
|
-
const relData = row[relation.relationName || key];
|
|
6564
|
-
if (relData === void 0 || relData === null) continue;
|
|
6565
|
-
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6566
|
-
const targetCollection = relation.target();
|
|
6567
|
-
normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
|
|
6568
|
-
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
|
|
6569
|
-
}
|
|
6570
|
-
return normalized;
|
|
6571
|
-
}
|
|
6572
|
-
/**
|
|
6573
|
-
* The row REST serves: every column under its own name, with the value Postgres
|
|
6574
|
-
* returned, and relations inlined as the target's columns.
|
|
6575
|
-
*
|
|
6576
|
-
* Values are the ones the database returned, except where that contradicts the
|
|
6577
|
-
* declared type: a `number` property is served as a number (see
|
|
6578
|
-
* {@link coerceDeclaredNumber}). Dates stay as the database returned them —
|
|
6579
|
-
* JSON has its own opinions about dates that the admin's view-model does not
|
|
6580
|
-
* share.
|
|
6581
|
-
*
|
|
6582
|
-
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
6583
|
-
* the relations `include` asked for, so the row is the authority on which are
|
|
6584
|
-
* actually there.
|
|
6585
|
-
*/
|
|
6586
|
-
function toRestRow(row, collection, registry) {
|
|
6587
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6588
|
-
const flat = {};
|
|
6589
|
-
for (const [key, value] of Object.entries(row)) {
|
|
6590
|
-
const relation = findRelation(resolvedRelations, key);
|
|
6591
|
-
if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
|
|
6592
|
-
else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
|
|
6593
|
-
else flat[key] = coerceDeclaredNumber(value, collection.properties?.[key]);
|
|
6594
|
-
}
|
|
6595
|
-
return flat;
|
|
6596
|
-
}
|
|
6597
|
-
//#endregion
|
|
6598
5773
|
//#region src/services/FetchService.ts
|
|
6599
5774
|
/**
|
|
6600
5775
|
* Service for handling all row read operations.
|
|
@@ -6653,7 +5828,7 @@ var FetchService = class {
|
|
|
6653
5828
|
if (!shouldInclude(key)) continue;
|
|
6654
5829
|
const drizzleRelName = relation.relationName || key;
|
|
6655
5830
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6656
|
-
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
5831
|
+
if (relation.cardinality === "many" && this.isJunctionRelation(relation, collection)) {
|
|
6657
5832
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6658
5833
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6659
5834
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6662,6 +5837,14 @@ var FetchService = class {
|
|
|
6662
5837
|
return withConfig;
|
|
6663
5838
|
}
|
|
6664
5839
|
/**
|
|
5840
|
+
* Detect if a many-to-many relation uses a junction table in the Drizzle schema.
|
|
5841
|
+
*/
|
|
5842
|
+
isJunctionRelation(relation, _collection) {
|
|
5843
|
+
if (relation.through) return true;
|
|
5844
|
+
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
5845
|
+
return false;
|
|
5846
|
+
}
|
|
5847
|
+
/**
|
|
6665
5848
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6666
5849
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6667
5850
|
*/
|
|
@@ -6670,6 +5853,55 @@ var FetchService = class {
|
|
|
6670
5853
|
return null;
|
|
6671
5854
|
}
|
|
6672
5855
|
/**
|
|
5856
|
+
* Convert a db.query result row (with nested relation objects) to a flat row.
|
|
5857
|
+
* Handles:
|
|
5858
|
+
* - Placing `id` at the top level as a string
|
|
5859
|
+
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
5860
|
+
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
5861
|
+
* - Flattening junction-table many-to-many results
|
|
5862
|
+
*/
|
|
5863
|
+
drizzleResultToRow(row, collection, _collectionPath, idInfo, _databaseId, idInfoArray) {
|
|
5864
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5865
|
+
const normalizedValues = normalizeDbValues(row, collection);
|
|
5866
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
5867
|
+
const relData = row[relation.relationName || key];
|
|
5868
|
+
if (relData === void 0 || relData === null) continue;
|
|
5869
|
+
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
5870
|
+
const targetCollection = relation.target();
|
|
5871
|
+
const targetPath = targetCollection.slug;
|
|
5872
|
+
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
5873
|
+
normalizedValues[key] = relData.map((item) => {
|
|
5874
|
+
let targetRow = item;
|
|
5875
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
5876
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
5877
|
+
if (nestedKey) targetRow = item[nestedKey];
|
|
5878
|
+
}
|
|
5879
|
+
const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
|
|
5880
|
+
return createRelationRefWithData(relId, targetPath, {
|
|
5881
|
+
id: relId,
|
|
5882
|
+
path: targetPath,
|
|
5883
|
+
values: normalizeDbValues(targetRow, targetCollection)
|
|
5884
|
+
});
|
|
5885
|
+
});
|
|
5886
|
+
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
5887
|
+
const targetCollection = relation.target();
|
|
5888
|
+
const targetPath = targetCollection.slug;
|
|
5889
|
+
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
5890
|
+
const relObj = relData;
|
|
5891
|
+
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
5892
|
+
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
5893
|
+
id: relId,
|
|
5894
|
+
path: targetPath,
|
|
5895
|
+
values: normalizeDbValues(relObj, targetCollection)
|
|
5896
|
+
});
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
return {
|
|
5900
|
+
...normalizedValues,
|
|
5901
|
+
id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
5902
|
+
};
|
|
5903
|
+
}
|
|
5904
|
+
/**
|
|
6673
5905
|
* Post-fetch joinPath relations for a single flat row.
|
|
6674
5906
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6675
5907
|
* so they must be loaded separately after the primary query.
|
|
@@ -6690,6 +5922,31 @@ var FetchService = class {
|
|
|
6690
5922
|
await Promise.all(promises);
|
|
6691
5923
|
}
|
|
6692
5924
|
/**
|
|
5925
|
+
* Post-fetch joinPath relations for a batch of flat rows.
|
|
5926
|
+
* Uses batch fetching to avoid N+1 queries for list views.
|
|
5927
|
+
*/
|
|
5928
|
+
async resolveJoinPathRelationsBatch(rows, collection, collectionPath, idInfo, _databaseId) {
|
|
5929
|
+
if (rows.length === 0) return;
|
|
5930
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5931
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
5932
|
+
if (joinPathRelations.length === 0) return;
|
|
5933
|
+
for (const [key, relation] of joinPathRelations) try {
|
|
5934
|
+
const rowIds = rows.map((r) => {
|
|
5935
|
+
return parseIdValues(String(r.id), [idInfo])[idInfo.fieldName];
|
|
5936
|
+
});
|
|
5937
|
+
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
5938
|
+
for (const row of rows) {
|
|
5939
|
+
const id = parseIdValues(String(row.id), [idInfo])[idInfo.fieldName];
|
|
5940
|
+
const relatedRow = resultMap.get(String(id));
|
|
5941
|
+
if (relatedRow) {
|
|
5942
|
+
if (relation.cardinality === "one") row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
} catch (e) {
|
|
5946
|
+
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
/**
|
|
6693
5950
|
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
6694
5951
|
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
6695
5952
|
*/
|
|
@@ -6700,29 +5957,72 @@ var FetchService = class {
|
|
|
6700
5957
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6701
5958
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6702
5959
|
if (joinPathRelations.length === 0) return;
|
|
6703
|
-
const
|
|
6704
|
-
const address = buildCompositeId(row, idInfoArray);
|
|
6705
|
-
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6706
|
-
};
|
|
5960
|
+
const idInfo = idInfoArray[0];
|
|
6707
5961
|
for (const [key, relation] of joinPathRelations) try {
|
|
6708
|
-
const
|
|
6709
|
-
|
|
6710
|
-
|
|
5962
|
+
const rowIds = rows.map((r) => {
|
|
5963
|
+
return parseIdValues(String(r.id), idInfoArray)[idInfo.fieldName];
|
|
5964
|
+
});
|
|
6711
5965
|
if (relation.cardinality === "one") {
|
|
6712
5966
|
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6713
|
-
for (const row of
|
|
6714
|
-
const
|
|
6715
|
-
|
|
5967
|
+
for (const row of rows) {
|
|
5968
|
+
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
5969
|
+
const relatedRow = resultMap.get(String(id));
|
|
5970
|
+
if (relatedRow) row[key] = {
|
|
5971
|
+
...relatedRow.values,
|
|
5972
|
+
id: relatedRow.id
|
|
5973
|
+
};
|
|
5974
|
+
else row[key] = null;
|
|
6716
5975
|
}
|
|
6717
5976
|
} else if (relation.cardinality === "many") {
|
|
6718
5977
|
const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, rowIds, key, relation);
|
|
6719
|
-
for (const row of
|
|
5978
|
+
for (const row of rows) {
|
|
5979
|
+
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
5980
|
+
row[key] = (resultMap.get(String(id)) || []).map((e) => ({
|
|
5981
|
+
...e.values,
|
|
5982
|
+
id: e.id
|
|
5983
|
+
}));
|
|
5984
|
+
}
|
|
6720
5985
|
}
|
|
6721
5986
|
} catch (e) {
|
|
6722
5987
|
logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
|
|
6723
5988
|
}
|
|
6724
5989
|
}
|
|
6725
5990
|
/**
|
|
5991
|
+
* Convert a db.query result row to a flat REST-style object with populated relations.
|
|
5992
|
+
*/
|
|
5993
|
+
drizzleResultToRestRow(row, collection, idInfo, idInfoArray) {
|
|
5994
|
+
const flat = { id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName]) };
|
|
5995
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
5996
|
+
for (const [k, v] of Object.entries(row)) {
|
|
5997
|
+
if (k === idInfo.fieldName) continue;
|
|
5998
|
+
const relation = findRelation(resolvedRelations, k);
|
|
5999
|
+
if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
|
|
6000
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6001
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6002
|
+
if (nestedKey) {
|
|
6003
|
+
const nested = item[nestedKey];
|
|
6004
|
+
return {
|
|
6005
|
+
...nested,
|
|
6006
|
+
id: String(nested.id ?? nested[Object.keys(nested)[0]])
|
|
6007
|
+
};
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
return {
|
|
6011
|
+
...item,
|
|
6012
|
+
id: String(item.id ?? item[Object.keys(item)[0]])
|
|
6013
|
+
};
|
|
6014
|
+
});
|
|
6015
|
+
else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) {
|
|
6016
|
+
const relObj = v;
|
|
6017
|
+
flat[k] = {
|
|
6018
|
+
...relObj,
|
|
6019
|
+
id: String(relObj.id ?? relObj[Object.keys(relObj)[0]])
|
|
6020
|
+
};
|
|
6021
|
+
} else flat[k] = v;
|
|
6022
|
+
}
|
|
6023
|
+
return flat;
|
|
6024
|
+
}
|
|
6025
|
+
/**
|
|
6726
6026
|
* Build db.query-compatible options from standard fetch options.
|
|
6727
6027
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6728
6028
|
*/
|
|
@@ -6792,7 +6092,7 @@ var FetchService = class {
|
|
|
6792
6092
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6793
6093
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6794
6094
|
const table = getTableForCollection(collection, this.registry);
|
|
6795
|
-
const idInfoArray =
|
|
6095
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6796
6096
|
const idInfo = idInfoArray[0];
|
|
6797
6097
|
const idField = table[idInfo.fieldName];
|
|
6798
6098
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6806,7 +6106,7 @@ var FetchService = class {
|
|
|
6806
6106
|
with: withConfig
|
|
6807
6107
|
});
|
|
6808
6108
|
if (!row) return void 0;
|
|
6809
|
-
const flatRow =
|
|
6109
|
+
const flatRow = this.drizzleResultToRow(row, collection, collectionPath, idInfo, databaseId, idInfoArray);
|
|
6810
6110
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6811
6111
|
return flatRow;
|
|
6812
6112
|
} catch (e) {
|
|
@@ -6848,7 +6148,7 @@ var FetchService = class {
|
|
|
6848
6148
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6849
6149
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6850
6150
|
const table = getTableForCollection(collection, this.registry);
|
|
6851
|
-
const idInfoArray =
|
|
6151
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6852
6152
|
const idInfo = idInfoArray[0];
|
|
6853
6153
|
const idField = table[idInfo.fieldName];
|
|
6854
6154
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6858,7 +6158,7 @@ var FetchService = class {
|
|
|
6858
6158
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6859
6159
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6860
6160
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6861
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6161
|
+
return (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRow(row, collection, collectionPath, idInfo, options.databaseId, idInfoArray));
|
|
6862
6162
|
} catch (e) {
|
|
6863
6163
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6864
6164
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6919,10 +6219,8 @@ var FetchService = class {
|
|
|
6919
6219
|
}
|
|
6920
6220
|
/**
|
|
6921
6221
|
* Fallback path used when db.query is unavailable.
|
|
6922
|
-
*
|
|
6923
|
-
*
|
|
6924
|
-
* relations from what drizzle already nested — no query per row. This one
|
|
6925
|
-
* has no nesting to read, so it resolves relations itself, in batches.
|
|
6222
|
+
* The primary path uses drizzleResultToRow which handles relation
|
|
6223
|
+
* mapping without N+1 queries.
|
|
6926
6224
|
*
|
|
6927
6225
|
* Process raw database results into flat rows with relations.
|
|
6928
6226
|
*/
|
|
@@ -6931,7 +6229,8 @@ var FetchService = class {
|
|
|
6931
6229
|
const parsedRows = await Promise.all(results.map(async (rawRow) => {
|
|
6932
6230
|
return {
|
|
6933
6231
|
rawRow,
|
|
6934
|
-
values: await parseDataFromServer(rawRow, collection)
|
|
6232
|
+
values: await parseDataFromServer(rawRow, collection),
|
|
6233
|
+
id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(rawRow, idInfoArray) : String(rawRow[idInfo.fieldName])
|
|
6935
6234
|
};
|
|
6936
6235
|
}));
|
|
6937
6236
|
if (!skipRelations) {
|
|
@@ -6971,7 +6270,10 @@ var FetchService = class {
|
|
|
6971
6270
|
logger.warn(`Could not batch load many relation property: ${key}`, { error: e });
|
|
6972
6271
|
}
|
|
6973
6272
|
}
|
|
6974
|
-
return parsedRows.map((item) =>
|
|
6273
|
+
return parsedRows.map((item) => ({
|
|
6274
|
+
...item.values,
|
|
6275
|
+
id: item.id
|
|
6276
|
+
}));
|
|
6975
6277
|
}
|
|
6976
6278
|
/**
|
|
6977
6279
|
* Fetch a collection of rows
|
|
@@ -7002,7 +6304,10 @@ var FetchService = class {
|
|
|
7002
6304
|
const relationKey = pathSegments[i];
|
|
7003
6305
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
7004
6306
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
7005
|
-
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6307
|
+
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6308
|
+
...row.values,
|
|
6309
|
+
id: row.id
|
|
6310
|
+
}));
|
|
7006
6311
|
if (i + 1 < pathSegments.length) {
|
|
7007
6312
|
const nextEntityId = pathSegments[i + 1];
|
|
7008
6313
|
currentCollection = relation.target();
|
|
@@ -7064,7 +6369,7 @@ var FetchService = class {
|
|
|
7064
6369
|
if (value === void 0 || value === null) return true;
|
|
7065
6370
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7066
6371
|
const table = getTableForCollection(collection, this.registry);
|
|
7067
|
-
const idInfoArray =
|
|
6372
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7068
6373
|
const idInfo = idInfoArray[0];
|
|
7069
6374
|
const idField = table[idInfo.fieldName];
|
|
7070
6375
|
const field = table[fieldName];
|
|
@@ -7091,7 +6396,7 @@ var FetchService = class {
|
|
|
7091
6396
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
7092
6397
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7093
6398
|
const table = getTableForCollection(collection, this.registry);
|
|
7094
|
-
const idInfoArray =
|
|
6399
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7095
6400
|
const idInfo = idInfoArray[0];
|
|
7096
6401
|
const idField = table[idInfo.fieldName];
|
|
7097
6402
|
const tableName = getTableName(table);
|
|
@@ -7099,7 +6404,7 @@ var FetchService = class {
|
|
|
7099
6404
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
7100
6405
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
7101
6406
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
7102
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
6407
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray));
|
|
7103
6408
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
7104
6409
|
return restRows;
|
|
7105
6410
|
} catch (e) {
|
|
@@ -7110,7 +6415,10 @@ var FetchService = class {
|
|
|
7110
6415
|
logger.warn(`[fetchCollectionForRest] db.query.findMany failed for ${collectionPath}, falling back`, { error: e });
|
|
7111
6416
|
}
|
|
7112
6417
|
const rows = await this.fetchRowsWithConditionsRaw(collectionPath, options);
|
|
7113
|
-
if (!include || include.length === 0) return rows
|
|
6418
|
+
if (!include || include.length === 0) return rows.map((row) => ({
|
|
6419
|
+
...row,
|
|
6420
|
+
id: idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
6421
|
+
}));
|
|
7114
6422
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
7115
6423
|
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
7116
6424
|
const shouldInclude = (key) => include[0] === "*" || include.includes(key);
|
|
@@ -7122,7 +6430,10 @@ var FetchService = class {
|
|
|
7122
6430
|
for (const row of rows) {
|
|
7123
6431
|
const eid = row[idInfo.fieldName];
|
|
7124
6432
|
const related = batchResults.get(String(eid));
|
|
7125
|
-
if (related) row[key] = {
|
|
6433
|
+
if (related) row[key] = {
|
|
6434
|
+
...related.values,
|
|
6435
|
+
id: related.id
|
|
6436
|
+
};
|
|
7126
6437
|
}
|
|
7127
6438
|
} catch (e) {
|
|
7128
6439
|
logger.warn(`[include] Failed to batch load one-to-one '${key}'`, { error: e });
|
|
@@ -7140,7 +6451,10 @@ var FetchService = class {
|
|
|
7140
6451
|
logger.warn(`[include] Failed to batch load many '${key}'`, { error: e });
|
|
7141
6452
|
}
|
|
7142
6453
|
}
|
|
7143
|
-
return rows
|
|
6454
|
+
return rows.map((row) => ({
|
|
6455
|
+
...row,
|
|
6456
|
+
id: idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName])
|
|
6457
|
+
}));
|
|
7144
6458
|
}
|
|
7145
6459
|
/**
|
|
7146
6460
|
* Fetch a single row with optional relation includes for REST API.
|
|
@@ -7148,7 +6462,7 @@ var FetchService = class {
|
|
|
7148
6462
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
7149
6463
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7150
6464
|
const table = getTableForCollection(collection, this.registry);
|
|
7151
|
-
const idInfoArray =
|
|
6465
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7152
6466
|
const idInfo = idInfoArray[0];
|
|
7153
6467
|
const idField = table[idInfo.fieldName];
|
|
7154
6468
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -7161,7 +6475,7 @@ var FetchService = class {
|
|
|
7161
6475
|
...withConfig ? { with: withConfig } : {}
|
|
7162
6476
|
});
|
|
7163
6477
|
if (!row) return null;
|
|
7164
|
-
const restRow =
|
|
6478
|
+
const restRow = this.drizzleResultToRestRow(row, collection, idInfo, idInfoArray);
|
|
7165
6479
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
7166
6480
|
return restRow;
|
|
7167
6481
|
} catch (e) {
|
|
@@ -7173,7 +6487,11 @@ var FetchService = class {
|
|
|
7173
6487
|
}
|
|
7174
6488
|
const result = await this.db.select().from(table).where(eq(idField, parsedId)).limit(1);
|
|
7175
6489
|
if (result.length === 0) return null;
|
|
7176
|
-
const
|
|
6490
|
+
const raw = result[0];
|
|
6491
|
+
const flatEntity = {
|
|
6492
|
+
...raw,
|
|
6493
|
+
id: idInfoArray.length > 1 ? buildCompositeId(raw, idInfoArray) : String(raw[idInfo.fieldName])
|
|
6494
|
+
};
|
|
7177
6495
|
if (!include || include.length === 0) return flatEntity;
|
|
7178
6496
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
7179
6497
|
const propertyKeys = new Set(Object.keys(collection.properties || {}));
|
|
@@ -7206,7 +6524,7 @@ var FetchService = class {
|
|
|
7206
6524
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7207
6525
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7208
6526
|
const table = getTableForCollection(collection, this.registry);
|
|
7209
|
-
const idField = table[
|
|
6527
|
+
const idField = table[getPrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7210
6528
|
let vectorMeta;
|
|
7211
6529
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7212
6530
|
let query = vectorMeta ? this.db.select({
|
|
@@ -7285,15 +6603,32 @@ var FetchService = class {
|
|
|
7285
6603
|
if (orderByField) queryOpts.orderBy = options.order === "asc" ? asc(orderByField) : desc(orderByField);
|
|
7286
6604
|
}
|
|
7287
6605
|
return (await queryTarget.findMany(queryOpts)).map((row) => {
|
|
7288
|
-
const flat = {};
|
|
7289
|
-
for (const [k, v] of Object.entries(row))
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
6606
|
+
const flat = { id: idInfoArray && idInfoArray.length > 1 ? buildCompositeId(row, idInfoArray) : String(row[idInfo.fieldName]) };
|
|
6607
|
+
for (const [k, v] of Object.entries(row)) {
|
|
6608
|
+
if (k === idInfo.fieldName) continue;
|
|
6609
|
+
if (Array.isArray(v)) flat[k] = v.map((item) => {
|
|
6610
|
+
const keys = Object.keys(item);
|
|
6611
|
+
const nestedObj = keys.find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6612
|
+
if (nestedObj && keys.length <= 3) {
|
|
6613
|
+
const nested = item[nestedObj];
|
|
6614
|
+
return {
|
|
6615
|
+
...nested,
|
|
6616
|
+
id: String(nested.id ?? nested[Object.keys(nested)[0]])
|
|
6617
|
+
};
|
|
6618
|
+
}
|
|
6619
|
+
return {
|
|
6620
|
+
...item,
|
|
6621
|
+
id: String(item.id ?? item[Object.keys(item)[0]])
|
|
6622
|
+
};
|
|
6623
|
+
});
|
|
6624
|
+
else if (typeof v === "object" && v !== null) {
|
|
6625
|
+
const relObj = v;
|
|
6626
|
+
flat[k] = {
|
|
6627
|
+
...relObj,
|
|
6628
|
+
id: String(relObj.id ?? relObj[Object.keys(relObj)[0]])
|
|
6629
|
+
};
|
|
6630
|
+
} else flat[k] = v;
|
|
6631
|
+
}
|
|
7297
6632
|
return flat;
|
|
7298
6633
|
});
|
|
7299
6634
|
} catch (e) {
|
|
@@ -7554,14 +6889,8 @@ var PersistService = class {
|
|
|
7554
6889
|
}
|
|
7555
6890
|
/**
|
|
7556
6891
|
* Save an row (create or update)
|
|
7557
|
-
*
|
|
7558
|
-
* With `options.upsert`, the row is written with INSERT ... ON CONFLICT DO
|
|
7559
|
-
* UPDATE against the primary key rather than a plain UPDATE. That is one
|
|
7560
|
-
* statement, so it cannot lose a race the way a read-then-write can, and it
|
|
7561
|
-
* does not care whether the row already exists — which is what a re-runnable
|
|
7562
|
-
* import needs.
|
|
7563
6892
|
*/
|
|
7564
|
-
async save(collectionPath, values, id, databaseId
|
|
6893
|
+
async save(collectionPath, values, id, databaseId) {
|
|
7565
6894
|
let effectiveCollectionPath = collectionPath;
|
|
7566
6895
|
const effectiveValues = { ...values };
|
|
7567
6896
|
let junctionTableInfo;
|
|
@@ -7652,7 +6981,7 @@ var PersistService = class {
|
|
|
7652
6981
|
const entityData = sanitizeAndConvertDates(serializedResult.scalarData);
|
|
7653
6982
|
savedId = await this.db.transaction(async (tx) => {
|
|
7654
6983
|
let currentId;
|
|
7655
|
-
if (id
|
|
6984
|
+
if (id) {
|
|
7656
6985
|
currentId = id;
|
|
7657
6986
|
const idValues = parseIdValues(id, idInfoArray);
|
|
7658
6987
|
if (joinPathRelationUpdates.length > 0) await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
|
|
@@ -7667,24 +6996,9 @@ var PersistService = class {
|
|
|
7667
6996
|
}
|
|
7668
6997
|
} else {
|
|
7669
6998
|
const dataForInsert = { ...entityData };
|
|
7670
|
-
if (id && options?.upsert) Object.assign(dataForInsert, parseIdValues(id, idInfoArray));
|
|
7671
6999
|
for (const info of idInfoArray) if (dataForInsert[info.fieldName] === "" || dataForInsert[info.fieldName] === null || dataForInsert[info.fieldName] === void 0) delete dataForInsert[info.fieldName];
|
|
7672
|
-
const
|
|
7673
|
-
|
|
7674
|
-
let result;
|
|
7675
|
-
if (options?.upsert && hasFullKey) {
|
|
7676
|
-
const target = idInfoArray.map((info) => table[info.fieldName]);
|
|
7677
|
-
const set = { ...dataForInsert };
|
|
7678
|
-
for (const info of idInfoArray) delete set[info.fieldName];
|
|
7679
|
-
result = Object.keys(set).length > 0 ? await insertQuery.onConflictDoUpdate({
|
|
7680
|
-
target,
|
|
7681
|
-
set
|
|
7682
|
-
}).returning(returningKeys) : await insertQuery.onConflictDoNothing({ target }).returning(returningKeys);
|
|
7683
|
-
} else result = await insertQuery.returning(returningKeys);
|
|
7684
|
-
const resultRow = result[0];
|
|
7685
|
-
if (!resultRow) if (id) currentId = id;
|
|
7686
|
-
else throw ApiError.forbidden(`Not allowed to write to "${effectiveCollectionPath}": the row was rejected by a row-level security policy.`, "WRITE_DENIED");
|
|
7687
|
-
else currentId = buildCompositeId(resultRow, idInfoArray);
|
|
7000
|
+
const resultRow = (await tx.insert(table).values(dataForInsert).returning(returningKeys))[0];
|
|
7001
|
+
currentId = buildCompositeId(resultRow, idInfoArray);
|
|
7688
7002
|
if (joinPathRelationUpdates.length > 0) await this.relationService.updateJoinPathOneToOneRelations(tx, collection, currentId, joinPathRelationUpdates);
|
|
7689
7003
|
}
|
|
7690
7004
|
if (inverseRelationUpdates.length > 0) await this.relationService.updateInverseRelations(tx, collection, currentId, inverseRelationUpdates);
|
|
@@ -7695,7 +7009,7 @@ var PersistService = class {
|
|
|
7695
7009
|
} catch (error) {
|
|
7696
7010
|
throw this.toUserFriendlyError(error, collection.slug);
|
|
7697
7011
|
}
|
|
7698
|
-
const finalEntity = await this.fetchService.
|
|
7012
|
+
const finalEntity = await this.fetchService.fetchOne(collection.slug, savedId, databaseId);
|
|
7699
7013
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
7700
7014
|
return finalEntity;
|
|
7701
7015
|
}
|
|
@@ -7793,8 +7107,8 @@ var DataService = class {
|
|
|
7793
7107
|
/**
|
|
7794
7108
|
* Save an row (create or update)
|
|
7795
7109
|
*/
|
|
7796
|
-
async save(collectionPath, values, id, databaseId
|
|
7797
|
-
return this.persistService.save(collectionPath, values, id, databaseId
|
|
7110
|
+
async save(collectionPath, values, id, databaseId) {
|
|
7111
|
+
return this.persistService.save(collectionPath, values, id, databaseId);
|
|
7798
7112
|
}
|
|
7799
7113
|
/**
|
|
7800
7114
|
* Delete an row by ID
|
|
@@ -7862,26 +7176,6 @@ var DataService = class {
|
|
|
7862
7176
|
*/
|
|
7863
7177
|
/** Internal prefix applied to branch database names to avoid collisions. */
|
|
7864
7178
|
var BRANCH_DB_PREFIX = "rb_";
|
|
7865
|
-
/** `duplicate_database` — the target database name is already taken. */
|
|
7866
|
-
var PG_DUPLICATE_DATABASE = "42P04";
|
|
7867
|
-
/** `object_in_use` — the database still has connections attached. */
|
|
7868
|
-
var PG_OBJECT_IN_USE = "55006";
|
|
7869
|
-
/**
|
|
7870
|
-
* Describe a failed branch DDL statement in terms a user can act on.
|
|
7871
|
-
*
|
|
7872
|
-
* Drizzle reports failures as `Failed query: <sql> params:` and hides the real
|
|
7873
|
-
* PostgreSQL error in the `cause` chain, so matching on `err.message` never sees
|
|
7874
|
-
* the actual problem. Match on the PG error code instead — it survives wrapping
|
|
7875
|
-
* and, unlike the message text, is not locale-dependent.
|
|
7876
|
-
*/
|
|
7877
|
-
function describeBranchDdlError(err, fallbackContext) {
|
|
7878
|
-
const pgError = extractPgError(err);
|
|
7879
|
-
if (pgError?.code === PG_DUPLICATE_DATABASE) return /* @__PURE__ */ new Error(`Database "${fallbackContext}" already exists on the server. Choose a different branch name.`);
|
|
7880
|
-
if (pgError?.code === PG_OBJECT_IN_USE) return /* @__PURE__ */ new Error(`Cannot complete the operation: the database "${fallbackContext}" has active connections. Close other clients or connections and try again.`);
|
|
7881
|
-
const detail = pgError?.message ?? extractCauseMessage(err);
|
|
7882
|
-
if (detail) return new Error(detail);
|
|
7883
|
-
return err instanceof Error ? err : new Error(String(err));
|
|
7884
|
-
}
|
|
7885
7179
|
/** Fully-qualified metadata table in the rebase schema. */
|
|
7886
7180
|
var BRANCHES_TABLE = "rebase.branches";
|
|
7887
7181
|
/**
|
|
@@ -7950,8 +7244,10 @@ var BranchService = class {
|
|
|
7950
7244
|
try {
|
|
7951
7245
|
await this.db.execute(sql.raw(`CREATE DATABASE "${safeDbName}" TEMPLATE "${safeSourceDb}"`));
|
|
7952
7246
|
} catch (err) {
|
|
7953
|
-
|
|
7954
|
-
throw
|
|
7247
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7248
|
+
if (msg.includes("already exists")) throw new Error(`Database "${dbName}" already exists on the server. Choose a different branch name.`);
|
|
7249
|
+
if (msg.includes("being accessed by other users")) throw new Error(`Cannot create branch: the source database "${sourceDb}" has active connections. Close other clients or connections and try again.`);
|
|
7250
|
+
throw err;
|
|
7955
7251
|
}
|
|
7956
7252
|
const now = /* @__PURE__ */ new Date();
|
|
7957
7253
|
await this.db.execute(sql`INSERT INTO rebase.branches (name, db_name, parent_db, created_at)
|
|
@@ -7976,8 +7272,8 @@ var BranchService = class {
|
|
|
7976
7272
|
try {
|
|
7977
7273
|
await this.db.execute(sql.raw(`DROP DATABASE "${safeDbName}"`));
|
|
7978
7274
|
} catch (err) {
|
|
7979
|
-
if (
|
|
7980
|
-
throw
|
|
7275
|
+
if ((err instanceof Error ? err.message : String(err)).includes("being accessed by other users")) throw new Error(`Cannot delete branch "${sanitizedName}": the database has active connections. Close other clients and try again.`);
|
|
7276
|
+
throw err;
|
|
7981
7277
|
}
|
|
7982
7278
|
await this.db.execute(sql`DELETE FROM rebase.branches WHERE name = ${sanitizedName}`);
|
|
7983
7279
|
}
|
|
@@ -8609,19 +7905,17 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8609
7905
|
this.realtimeService.subscriptions.delete(subscriptionId);
|
|
8610
7906
|
};
|
|
8611
7907
|
}
|
|
8612
|
-
async save({ path, id, values, collection, status
|
|
7908
|
+
async save({ path, id, values, collection, status }) {
|
|
8613
7909
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, path);
|
|
8614
7910
|
let updatedValues = values;
|
|
8615
7911
|
const contextForCallback = this.buildCallContext();
|
|
8616
7912
|
let previousValuesForHistory;
|
|
8617
|
-
if (status === "existing" && id)
|
|
8618
|
-
const existing = await this.dataService.
|
|
7913
|
+
if (status === "existing" && id) {
|
|
7914
|
+
const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
|
|
8619
7915
|
if (existing) {
|
|
8620
7916
|
const { id: _existingId, ...existingValues } = existing;
|
|
8621
7917
|
previousValuesForHistory = existingValues;
|
|
8622
7918
|
}
|
|
8623
|
-
} catch (err) {
|
|
8624
|
-
logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
|
|
8625
7919
|
}
|
|
8626
7920
|
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
8627
7921
|
if (globalCallbacks?.beforeSave) {
|
|
@@ -8668,7 +7962,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8668
7962
|
timestampNowValue: /* @__PURE__ */ new Date()
|
|
8669
7963
|
});
|
|
8670
7964
|
try {
|
|
8671
|
-
let savedRow = await this.dataService.save(path, updatedValues, id, resolvedCollection?.databaseId
|
|
7965
|
+
let savedRow = await this.dataService.save(path, updatedValues, id, resolvedCollection?.databaseId);
|
|
8672
7966
|
if (savedRow && (globalCallbacks?.afterRead || callbacks?.afterRead || propertyCallbacks?.afterRead)) {
|
|
8673
7967
|
if (globalCallbacks?.afterRead) savedRow = await globalCallbacks.afterRead({
|
|
8674
7968
|
collection: resolvedCollection,
|
|
@@ -8689,8 +7983,8 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8689
7983
|
context: contextForCallback
|
|
8690
7984
|
});
|
|
8691
7985
|
}
|
|
8692
|
-
const savedId =
|
|
8693
|
-
const savedValues = savedRow;
|
|
7986
|
+
const savedId = savedRow.id;
|
|
7987
|
+
const { id: _savedId, ...savedValues } = savedRow;
|
|
8694
7988
|
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
8695
7989
|
if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
|
|
8696
7990
|
collection: resolvedCollection,
|
|
@@ -8722,7 +8016,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8722
8016
|
}
|
|
8723
8017
|
if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
|
|
8724
8018
|
tableName: path,
|
|
8725
|
-
id: savedId,
|
|
8019
|
+
id: savedId.toString(),
|
|
8726
8020
|
action: status === "new" ? "create" : "update",
|
|
8727
8021
|
values: savedValues,
|
|
8728
8022
|
previousValues: previousValuesForHistory,
|
|
@@ -8730,11 +8024,11 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8730
8024
|
});
|
|
8731
8025
|
if (this._deferNotifications) this._pendingNotifications.push({
|
|
8732
8026
|
path,
|
|
8733
|
-
id: savedId,
|
|
8027
|
+
id: savedId.toString(),
|
|
8734
8028
|
row: savedRow,
|
|
8735
8029
|
databaseId: resolvedCollection?.databaseId
|
|
8736
8030
|
});
|
|
8737
|
-
else await this.realtimeService.notifyUpdate(path, savedId, savedRow, resolvedCollection?.databaseId);
|
|
8031
|
+
else await this.realtimeService.notifyUpdate(path, savedId.toString(), savedRow, resolvedCollection?.databaseId);
|
|
8738
8032
|
return savedRow;
|
|
8739
8033
|
} catch (error) {
|
|
8740
8034
|
if (globalCallbacks?.afterSaveError || callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
@@ -8769,52 +8063,12 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8769
8063
|
throw error;
|
|
8770
8064
|
}
|
|
8771
8065
|
}
|
|
8772
|
-
/**
|
|
8773
|
-
* Write many rows through the same pipeline as {@link save}.
|
|
8774
|
-
*
|
|
8775
|
-
* The batch runs in one transaction of its own, so a failure part-way leaves
|
|
8776
|
-
* nothing behind — the point of a batch is that a re-run starts from a known
|
|
8777
|
-
* state. When this driver is already inside a transaction (the authenticated
|
|
8778
|
-
* path, via `withTransaction`) the nested call becomes a savepoint, which is
|
|
8779
|
-
* still atomic and still commits once.
|
|
8780
|
-
*
|
|
8781
|
-
* Rows are applied in order, so a batch that touches the same key twice ends
|
|
8782
|
-
* with the last write winning, exactly as separate calls would.
|
|
8783
|
-
*/
|
|
8784
|
-
async saveMany({ path, rows, collection, upsert }) {
|
|
8785
|
-
return this.db.transaction(async (tx) => {
|
|
8786
|
-
const txDriver = new PostgresBackendDriver(tx, this.realtimeService, this.registry, this.user, this.poolManager, this.historyService);
|
|
8787
|
-
txDriver.dataService = new DataService(tx, this.registry);
|
|
8788
|
-
txDriver.client = this.client;
|
|
8789
|
-
txDriver._deferNotifications = this._deferNotifications;
|
|
8790
|
-
txDriver._pendingNotifications = this._pendingNotifications;
|
|
8791
|
-
const saved = [];
|
|
8792
|
-
for (let i = 0; i < rows.length; i++) {
|
|
8793
|
-
const values = rows[i];
|
|
8794
|
-
const id = values?.id;
|
|
8795
|
-
try {
|
|
8796
|
-
saved.push(await txDriver.save({
|
|
8797
|
-
path,
|
|
8798
|
-
values,
|
|
8799
|
-
collection,
|
|
8800
|
-
status: "new",
|
|
8801
|
-
upsert
|
|
8802
|
-
}));
|
|
8803
|
-
} catch (error) {
|
|
8804
|
-
const label = id !== void 0 ? `id ${JSON.stringify(id)}` : "no id";
|
|
8805
|
-
throw Object.assign(new Error(`Row ${i} of ${rows.length} (${label}) failed: ${error?.message ?? error}`, { cause: error }), {
|
|
8806
|
-
statusCode: error?.statusCode,
|
|
8807
|
-
code: error?.code,
|
|
8808
|
-
name: error?.name
|
|
8809
|
-
});
|
|
8810
|
-
}
|
|
8811
|
-
}
|
|
8812
|
-
return saved;
|
|
8813
|
-
});
|
|
8814
|
-
}
|
|
8815
8066
|
async delete({ row, collection }) {
|
|
8816
8067
|
const targetPath = row.path;
|
|
8817
|
-
const targetRow = {
|
|
8068
|
+
const targetRow = {
|
|
8069
|
+
id: row.id,
|
|
8070
|
+
...row.values ?? {}
|
|
8071
|
+
};
|
|
8818
8072
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, targetPath);
|
|
8819
8073
|
const contextForCallback = this.buildCallContext();
|
|
8820
8074
|
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
@@ -9173,18 +8427,6 @@ var AuthenticatedPostgresBackendDriver = class {
|
|
|
9173
8427
|
async save(props) {
|
|
9174
8428
|
return this.withTransaction((delegate) => delegate.save(props));
|
|
9175
8429
|
}
|
|
9176
|
-
/**
|
|
9177
|
-
* One transaction for the whole batch, rather than one per row.
|
|
9178
|
-
*
|
|
9179
|
-
* This is the point of the method: `save` opens a transaction per call, so
|
|
9180
|
-
* importing 10k rows through it means 10k transactions (and, over HTTP, 10k
|
|
9181
|
-
* round trips). Here the RLS context is established once and every row lands
|
|
9182
|
-
* or none does. Realtime notifications are already deferred to commit by
|
|
9183
|
-
* `withTransaction`, so a batch does not flood subscribers mid-flight.
|
|
9184
|
-
*/
|
|
9185
|
-
async saveMany(props) {
|
|
9186
|
-
return this.withTransaction((delegate) => delegate.saveMany(props));
|
|
9187
|
-
}
|
|
9188
8430
|
async delete(props) {
|
|
9189
8431
|
return this.withTransaction((delegate) => delegate.delete(props));
|
|
9190
8432
|
}
|
|
@@ -9234,7 +8476,6 @@ var DatabasePoolManager = class {
|
|
|
9234
8476
|
pool.on("error", (err) => {
|
|
9235
8477
|
logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
|
|
9236
8478
|
});
|
|
9237
|
-
guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
|
|
9238
8479
|
this.pools.set(databaseName, pool);
|
|
9239
8480
|
return pool;
|
|
9240
8481
|
}
|
|
@@ -9432,6 +8673,105 @@ var magicLinkTokensRelations = relations(magicLinkTokens, ({ one }) => ({ user:
|
|
|
9432
8673
|
references: [users.id]
|
|
9433
8674
|
}) }));
|
|
9434
8675
|
//#endregion
|
|
8676
|
+
//#region src/schema/auth-default-policies.ts
|
|
8677
|
+
/**
|
|
8678
|
+
* Default RLS policies injected by the schema generator.
|
|
8679
|
+
*
|
|
8680
|
+
* Rebase's enforcement model is unified: authenticated (user-context) requests
|
|
8681
|
+
* run under the restricted `rebase_user` role, so Postgres RLS binds *every*
|
|
8682
|
+
* statement — reads and writes. A collection's `securityRules` are the whole
|
|
8683
|
+
* authorization model. The server context (auth flows, migrations,
|
|
8684
|
+
* `dataAsAdmin`) runs as the owner and bypasses RLS.
|
|
8685
|
+
*
|
|
8686
|
+
* Because RLS default-denies, every collection is **locked by default**: with
|
|
8687
|
+
* no rules, only the server context and admins can touch it. The generator
|
|
8688
|
+
* injects that safe baseline:
|
|
8689
|
+
*
|
|
8690
|
+
* **For every collection**
|
|
8691
|
+
* 1. A permissive **server-or-admin SELECT** grant.
|
|
8692
|
+
* 2. A permissive **server-or-admin write** grant (insert/update/delete).
|
|
8693
|
+
*
|
|
8694
|
+
* Author `securityRules` are permissive and OR together, so explicit rules only
|
|
8695
|
+
* *broaden* access from this locked baseline (e.g. "users read/write their own
|
|
8696
|
+
* rows").
|
|
8697
|
+
*
|
|
8698
|
+
* **For auth collections additionally**
|
|
8699
|
+
* 3. A permissive **self SELECT** grant (`id = auth.uid()`), so users can read
|
|
8700
|
+
* their own row (profile, session bootstrap) without every app re-declaring
|
|
8701
|
+
* it.
|
|
8702
|
+
* 4. A **restrictive** admin write gate. Restrictive policies are AND'd with
|
|
8703
|
+
* every other policy, so a write is rejected unless the caller is an admin
|
|
8704
|
+
* (or the server context) — even if the author also wrote a permissive rule
|
|
8705
|
+
* such as "a user may edit their own row". Without this, a permissive owner
|
|
8706
|
+
* rule would let a user change their own `roles`.
|
|
8707
|
+
*
|
|
8708
|
+
* The server context is recognised as `auth.uid() IS NULL` (`policy.serverContext()`)
|
|
8709
|
+
* — the built-in flows that run without a user (signup, migrations) set no user
|
|
8710
|
+
* GUC — which also lets the owner connection satisfy these policies even under
|
|
8711
|
+
* FORCE RLS. A *user* request never reaches that state: an anonymous one carries
|
|
8712
|
+
* `ANONYMOUS_USER_ID`, precisely so it cannot pass for the server here.
|
|
8713
|
+
*
|
|
8714
|
+
* Opt out with `disableDefaultPolicies: true` to take full responsibility for
|
|
8715
|
+
* the collection's RLS.
|
|
8716
|
+
*/
|
|
8717
|
+
var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
8718
|
+
/** Write operations that must be admin-gated by default on auth collections. */
|
|
8719
|
+
var DEFAULT_GUARDED_OPS = [
|
|
8720
|
+
"insert",
|
|
8721
|
+
"update",
|
|
8722
|
+
"delete"
|
|
8723
|
+
];
|
|
8724
|
+
/** Whether a collection is flagged as an authentication collection. */
|
|
8725
|
+
function isAuthCollection(collection) {
|
|
8726
|
+
const auth = collection.auth;
|
|
8727
|
+
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
8728
|
+
}
|
|
8729
|
+
/** The property marked as the row id (falls back to `id`). */
|
|
8730
|
+
function getIdPropertyName(collection) {
|
|
8731
|
+
for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
|
|
8732
|
+
return "id";
|
|
8733
|
+
}
|
|
8734
|
+
/**
|
|
8735
|
+
* Returns the security rules that should be applied to a collection: the
|
|
8736
|
+
* author's explicit `securityRules` plus the framework defaults described in
|
|
8737
|
+
* the module doc (baseline server/admin read for all collections; self-read
|
|
8738
|
+
* and the admin write gate for auth collections).
|
|
8739
|
+
*
|
|
8740
|
+
* Collections that opt out via `disableDefaultPolicies` are returned unchanged.
|
|
8741
|
+
*/
|
|
8742
|
+
function getEffectiveSecurityRules(collection) {
|
|
8743
|
+
const explicit = [...(isPostgresCollectionConfig(collection) ? collection.securityRules : void 0) ?? []];
|
|
8744
|
+
if (collection.disableDefaultPolicies) return explicit;
|
|
8745
|
+
const tableName = getTableName$1(collection);
|
|
8746
|
+
const injected = [];
|
|
8747
|
+
injected.push({
|
|
8748
|
+
name: `${tableName}_default_admin_read`,
|
|
8749
|
+
operations: ["select"],
|
|
8750
|
+
condition: SERVER_OR_ADMIN_EXPR
|
|
8751
|
+
});
|
|
8752
|
+
injected.push({
|
|
8753
|
+
name: `${tableName}_default_admin_write`,
|
|
8754
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
8755
|
+
condition: SERVER_OR_ADMIN_EXPR,
|
|
8756
|
+
check: SERVER_OR_ADMIN_EXPR
|
|
8757
|
+
});
|
|
8758
|
+
if (isAuthCollection(collection)) {
|
|
8759
|
+
injected.push({
|
|
8760
|
+
name: `${tableName}_default_self_read`,
|
|
8761
|
+
operations: ["select"],
|
|
8762
|
+
condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
|
|
8763
|
+
});
|
|
8764
|
+
injected.push({
|
|
8765
|
+
name: `${tableName}_require_admin_write`,
|
|
8766
|
+
mode: "restrictive",
|
|
8767
|
+
operations: [...DEFAULT_GUARDED_OPS],
|
|
8768
|
+
condition: SERVER_OR_ADMIN_EXPR,
|
|
8769
|
+
check: SERVER_OR_ADMIN_EXPR
|
|
8770
|
+
});
|
|
8771
|
+
}
|
|
8772
|
+
return [...explicit, ...injected];
|
|
8773
|
+
}
|
|
8774
|
+
//#endregion
|
|
9435
8775
|
//#region src/schema/generate-drizzle-schema-logic.ts
|
|
9436
8776
|
/**
|
|
9437
8777
|
* Resolve the SQL column name for a property.
|
|
@@ -9629,12 +8969,31 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
9629
8969
|
* Wraps a compiled SQL clause in a Drizzle `sql\`...\`` template literal.
|
|
9630
8970
|
*/
|
|
9631
8971
|
var wrapSql = (clause) => `sql\`${clause}\``;
|
|
8972
|
+
/**
|
|
8973
|
+
* Generates a deterministic hash based on the rule configuration.
|
|
8974
|
+
*/
|
|
8975
|
+
var getPolicyNameHash = (rule) => {
|
|
8976
|
+
const data = JSON.stringify({
|
|
8977
|
+
a: rule.access,
|
|
8978
|
+
m: rule.mode,
|
|
8979
|
+
op: rule.operation,
|
|
8980
|
+
ops: rule.operations?.slice().sort(),
|
|
8981
|
+
own: rule.ownerField,
|
|
8982
|
+
rol: rule.roles?.slice().sort(),
|
|
8983
|
+
pg: rule.pgRoles?.slice().sort(),
|
|
8984
|
+
u: rule.using,
|
|
8985
|
+
w: rule.withCheck,
|
|
8986
|
+
c: rule.condition,
|
|
8987
|
+
ch: rule.check
|
|
8988
|
+
});
|
|
8989
|
+
return createHash("sha1").update(data).digest("hex").substring(0, 7);
|
|
8990
|
+
};
|
|
9632
8991
|
var generatePolicyCode = (collection, rule, index, resolveCollection) => {
|
|
9633
8992
|
const tableName = getTableName$1(collection);
|
|
9634
8993
|
const ops = rule.operations && rule.operations.length > 0 ? rule.operations : [rule.operation ?? "all"];
|
|
9635
|
-
const
|
|
8994
|
+
const ruleHash = getPolicyNameHash(rule);
|
|
9636
8995
|
return ops.map((op, opIdx) => {
|
|
9637
|
-
return generateSinglePolicyCode(collection, rule, op,
|
|
8996
|
+
return generateSinglePolicyCode(collection, rule, op, rule.name ? ops.length > 1 ? `${rule.name}_${op}` : rule.name : `${tableName}_${op}_${ruleHash}${ops.length > 1 ? `_${opIdx}` : ""}`, resolveCollection);
|
|
9638
8997
|
}).join("");
|
|
9639
8998
|
};
|
|
9640
8999
|
/**
|
|
@@ -9763,7 +9122,6 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
9763
9122
|
});
|
|
9764
9123
|
});
|
|
9765
9124
|
schemaContent += "\n";
|
|
9766
|
-
const junctionSpecs = resolveJunctionSpecs(collections);
|
|
9767
9125
|
for (const collection of collections) {
|
|
9768
9126
|
const tableName = getTableName$1(collection);
|
|
9769
9127
|
if (tableName) allTablesToGenerate.set(tableName, { collection });
|
|
@@ -9797,17 +9155,9 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
9797
9155
|
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
9798
9156
|
schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName$1(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
|
|
9799
9157
|
schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName$1(targetCollection))}.${targetId}, ${refOptions}),\n`;
|
|
9800
|
-
schemaContent += "}, (table) => (
|
|
9801
|
-
schemaContent += ` primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })
|
|
9802
|
-
|
|
9803
|
-
if (!stripPolicies && junctionSpec) {
|
|
9804
|
-
const junctionCollection = getJunctionCollectionConfig(junctionSpec);
|
|
9805
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName$1(c) === slug);
|
|
9806
|
-
getJunctionSecurityRules(junctionSpec).forEach((rule, idx) => {
|
|
9807
|
-
schemaContent += generatePolicyCode(junctionCollection, rule, idx, resolveCollection);
|
|
9808
|
-
});
|
|
9809
|
-
}
|
|
9810
|
-
schemaContent += "])).enableRLS();\n\n";
|
|
9158
|
+
schemaContent += "}, (table) => ({\n";
|
|
9159
|
+
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
|
|
9160
|
+
schemaContent += "}));\n\n";
|
|
9811
9161
|
} else if (!isJunction) {
|
|
9812
9162
|
const schema = isPostgresCollectionConfig(collection) ? collection.schema : void 0;
|
|
9813
9163
|
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
@@ -10498,7 +9848,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10498
9848
|
startAfter: request.startAfter,
|
|
10499
9849
|
searchString: request.searchString
|
|
10500
9850
|
}, authContext);
|
|
10501
|
-
this.sendCollectionUpdate(clientId, subscriptionId, rows
|
|
9851
|
+
this.sendCollectionUpdate(clientId, subscriptionId, rows);
|
|
10502
9852
|
} catch (error) {
|
|
10503
9853
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
10504
9854
|
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10599,7 +9949,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10599
9949
|
if (subscription.type === "single" && notifyPath === originalPath) if (row && row?._rebase_invalidated) this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);
|
|
10600
9950
|
else this.sendSingleUpdate(subscription.clientId, subscriptionId, row);
|
|
10601
9951
|
else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
10602
|
-
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row
|
|
9952
|
+
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row);
|
|
10603
9953
|
this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
|
|
10604
9954
|
}
|
|
10605
9955
|
} catch (error) {
|
|
@@ -10629,7 +9979,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10629
9979
|
if (!this._subscriptions.has(subscriptionId)) return;
|
|
10630
9980
|
try {
|
|
10631
9981
|
const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
|
|
10632
|
-
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows
|
|
9982
|
+
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows);
|
|
10633
9983
|
} catch (error) {
|
|
10634
9984
|
const sanitized = sanitizeErrorForClient(error, notifyPath);
|
|
10635
9985
|
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10843,12 +10193,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10843
10193
|
}
|
|
10844
10194
|
return await this.dataService.fetchOne(notifyPath, id);
|
|
10845
10195
|
}
|
|
10846
|
-
sendCollectionUpdate(clientId, subscriptionId, rows
|
|
10196
|
+
sendCollectionUpdate(clientId, subscriptionId, rows) {
|
|
10847
10197
|
const message = {
|
|
10848
10198
|
type: "collection_update",
|
|
10849
10199
|
subscriptionId,
|
|
10850
|
-
rows
|
|
10851
|
-
pks: this.primaryKeysForPath(path)
|
|
10200
|
+
rows
|
|
10852
10201
|
};
|
|
10853
10202
|
this.sendMessage(clientId, message);
|
|
10854
10203
|
}
|
|
@@ -10863,33 +10212,16 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10863
10212
|
/**
|
|
10864
10213
|
* Send a lightweight row-level patch to a collection subscriber.
|
|
10865
10214
|
* The client can merge this into its cached data for instant feedback.
|
|
10866
|
-
*
|
|
10867
|
-
* The key columns ride along: the patch names a row by address, and the
|
|
10868
|
-
* client has to find that row among the ones it cached — which carry
|
|
10869
|
-
* columns and no address. The SDK holds no collection config to derive one
|
|
10870
|
-
* from, so this is the only place the mapping can come from.
|
|
10871
10215
|
*/
|
|
10872
|
-
sendCollectionPatch(clientId, subscriptionId, id, row
|
|
10216
|
+
sendCollectionPatch(clientId, subscriptionId, id, row) {
|
|
10873
10217
|
const message = {
|
|
10874
10218
|
type: "collection_patch",
|
|
10875
10219
|
subscriptionId,
|
|
10876
10220
|
id,
|
|
10877
|
-
row
|
|
10878
|
-
pks: this.primaryKeysForPath(notifyPath)
|
|
10221
|
+
row
|
|
10879
10222
|
};
|
|
10880
10223
|
this.sendMessage(clientId, message);
|
|
10881
10224
|
}
|
|
10882
|
-
/** The key columns of the collection at `path`, if they can be resolved. */
|
|
10883
|
-
primaryKeysForPath(path) {
|
|
10884
|
-
try {
|
|
10885
|
-
const collection = this.registry.getCollectionByPath(path);
|
|
10886
|
-
if (!collection) return void 0;
|
|
10887
|
-
const keys = getPrimaryKeys(collection, this.registry);
|
|
10888
|
-
return keys.length > 0 ? keys : void 0;
|
|
10889
|
-
} catch {
|
|
10890
|
-
return;
|
|
10891
|
-
}
|
|
10892
|
-
}
|
|
10893
10225
|
sendError(clientId, error, subscriptionId, code) {
|
|
10894
10226
|
const message = {
|
|
10895
10227
|
type: "error",
|
|
@@ -11137,7 +10469,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
11137
10469
|
}
|
|
11138
10470
|
/** Compute the canonical (possibly composite) id string from a captured row. */
|
|
11139
10471
|
extractIdFromCdcRow(collection, row) {
|
|
11140
|
-
|
|
10472
|
+
try {
|
|
10473
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, this.registry));
|
|
10474
|
+
if (composite && composite !== ":::") return composite;
|
|
10475
|
+
} catch {}
|
|
10476
|
+
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
10477
|
+
return "*";
|
|
11141
10478
|
}
|
|
11142
10479
|
dedupKey(path, id, databaseId) {
|
|
11143
10480
|
return `${databaseId ?? ""}::${path}::${id}`;
|
|
@@ -11753,7 +11090,7 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
|
|
|
11753
11090
|
} catch (error) {
|
|
11754
11091
|
logger.error("💥 [WebSocket Server] Error handling message", { error });
|
|
11755
11092
|
if (error instanceof Error) logger.error("Stack trace", { detail: error.stack });
|
|
11756
|
-
const errorMessage = process.env.NODE_ENV === "production" ? "An unexpected error occurred" :
|
|
11093
|
+
const errorMessage = process.env.NODE_ENV === "production" ? "An unexpected error occurred" : error instanceof Error ? error.message : "An unexpected error occurred";
|
|
11757
11094
|
const errorResponse = {
|
|
11758
11095
|
type: "ERROR",
|
|
11759
11096
|
requestId,
|
|
@@ -20597,33 +19934,6 @@ function formatBytes(bytes) {
|
|
|
20597
19934
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
20598
19935
|
}
|
|
20599
19936
|
//#endregion
|
|
20600
|
-
//#region src/collections/buildRegistry.ts
|
|
20601
|
-
/**
|
|
20602
|
-
* Build the collection registry for a driver.
|
|
20603
|
-
*
|
|
20604
|
-
* The order matters and is the reason this is one function rather than a run of
|
|
20605
|
-
* statements in the bootstrapper. Keys are resolved from the drizzle schema, so
|
|
20606
|
-
* anything that inspects them has to run *after* the tables are registered —
|
|
20607
|
-
* and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
|
|
20608
|
-
* collection whose table it cannot look up is one it has nothing to say about.
|
|
20609
|
-
* Warned too early, it would skip every collection and report nothing, which
|
|
20610
|
-
* reads exactly like having nothing to report.
|
|
20611
|
-
*/
|
|
20612
|
-
function buildCollectionRegistry(schema) {
|
|
20613
|
-
const registry = new PostgresCollectionRegistry();
|
|
20614
|
-
if (schema.collections) {
|
|
20615
|
-
registry.registerMultiple(schema.collections);
|
|
20616
|
-
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
20617
|
-
}
|
|
20618
|
-
if (schema.tables) Object.values(schema.tables).forEach((table) => {
|
|
20619
|
-
if (isTable(table)) registry.registerTable(table, getTableName(table));
|
|
20620
|
-
});
|
|
20621
|
-
if (schema.enums) registry.registerEnums(schema.enums);
|
|
20622
|
-
if (schema.relations) registry.registerRelations(schema.relations);
|
|
20623
|
-
warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
|
|
20624
|
-
return registry;
|
|
20625
|
-
}
|
|
20626
|
-
//#endregion
|
|
20627
19937
|
//#region src/auth/ensure-tables.ts
|
|
20628
19938
|
/**
|
|
20629
19939
|
* Auto-create auth tables if they don't exist.
|
|
@@ -20795,22 +20105,14 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20795
20105
|
$$ LANGUAGE sql STABLE
|
|
20796
20106
|
`);
|
|
20797
20107
|
});
|
|
20798
|
-
|
|
20799
|
-
|
|
20800
|
-
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
20807
|
-
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
20808
|
-
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
|
|
20809
|
-
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
|
|
20810
|
-
]) await db.execute(sql`
|
|
20811
|
-
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20812
|
-
ADD COLUMN IF NOT EXISTS ${sql.raw(columnDef)}
|
|
20813
|
-
`);
|
|
20108
|
+
await db.execute(sql`
|
|
20109
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20110
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
20111
|
+
`);
|
|
20112
|
+
await db.execute(sql`
|
|
20113
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20114
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
20115
|
+
`);
|
|
20814
20116
|
try {
|
|
20815
20117
|
if ((await db.execute(sql`
|
|
20816
20118
|
SELECT EXISTS (
|
|
@@ -20881,34 +20183,6 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20881
20183
|
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
20882
20184
|
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
20883
20185
|
`);
|
|
20884
|
-
try {
|
|
20885
|
-
const authTablePairs = [
|
|
20886
|
-
[usersSchema, resolvedTable],
|
|
20887
|
-
[authSchema, "user_identities"],
|
|
20888
|
-
[authSchema, "refresh_tokens"],
|
|
20889
|
-
[authSchema, "password_reset_tokens"],
|
|
20890
|
-
[authSchema, "app_config"],
|
|
20891
|
-
[authSchema, "mfa_factors"],
|
|
20892
|
-
[authSchema, "mfa_challenges"],
|
|
20893
|
-
[authSchema, "recovery_codes"]
|
|
20894
|
-
];
|
|
20895
|
-
for (const [schemaName, tableName] of authTablePairs) if ((await db.execute(sql`
|
|
20896
|
-
SELECT 1
|
|
20897
|
-
FROM pg_class c
|
|
20898
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
20899
|
-
WHERE n.nspname = ${schemaName}
|
|
20900
|
-
AND c.relname = ${tableName}
|
|
20901
|
-
AND c.relforcerowsecurity
|
|
20902
|
-
`)).rows.length > 0) {
|
|
20903
|
-
await db.execute(sql`
|
|
20904
|
-
ALTER TABLE ${sql.raw(`"${schemaName}"."${tableName}"`)}
|
|
20905
|
-
NO FORCE ROW LEVEL SECURITY
|
|
20906
|
-
`);
|
|
20907
|
-
logger.warn(`🔧 Cleared stale FORCE ROW LEVEL SECURITY on "${schemaName}"."${tableName}" (legacy RLS model — it binds the owner connection and breaks privileged auth writes)`);
|
|
20908
|
-
}
|
|
20909
|
-
} catch (rlsReconcileError) {
|
|
20910
|
-
logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
|
|
20911
|
-
}
|
|
20912
20186
|
logger.info("✅ Auth tables ready");
|
|
20913
20187
|
} catch (error) {
|
|
20914
20188
|
logger.error("❌ Failed to create auth tables", { error });
|
|
@@ -20956,31 +20230,6 @@ var UserService = class {
|
|
|
20956
20230
|
const name = getTableName(this.usersTable);
|
|
20957
20231
|
return `"${getTableConfig(this.usersTable).schema || "public"}"."${name}"`;
|
|
20958
20232
|
}
|
|
20959
|
-
/**
|
|
20960
|
-
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
20961
|
-
*
|
|
20962
|
-
* The auth services run on the base/owner connection, which by design
|
|
20963
|
-
* carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
|
|
20964
|
-
* in the default policies applies. That NULL is normally guaranteed by
|
|
20965
|
-
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
20966
|
-
* GUC that survives on a pooled connection (or a connection role that
|
|
20967
|
-
* doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
|
|
20968
|
-
* turns the trusted write into an RLS-scoped one and denies it with
|
|
20969
|
-
* SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
|
|
20970
|
-
* single chokepoint, makes the server context deterministic instead of
|
|
20971
|
-
* trusting whatever state the pool hands us. `auth.uid()` reads '' as
|
|
20972
|
-
* NULL via NULLIF, so '' is the server context.
|
|
20973
|
-
*/
|
|
20974
|
-
async withServerContext(fn) {
|
|
20975
|
-
return await this.db.transaction(async (tx) => {
|
|
20976
|
-
await tx.execute(sql`
|
|
20977
|
-
SELECT set_config('app.user_id', '', true),
|
|
20978
|
-
set_config('app.user_roles', '', true),
|
|
20979
|
-
set_config('app.jwt', '', true)
|
|
20980
|
-
`);
|
|
20981
|
-
return await fn(tx);
|
|
20982
|
-
});
|
|
20983
|
-
}
|
|
20984
20233
|
mapRowToUser(row) {
|
|
20985
20234
|
if (!row) return row;
|
|
20986
20235
|
const id = row.id ?? row.uid;
|
|
@@ -21078,7 +20327,7 @@ var UserService = class {
|
|
|
21078
20327
|
}
|
|
21079
20328
|
async createUser(data) {
|
|
21080
20329
|
const payload = this.mapPayload(data);
|
|
21081
|
-
const [row] = await this.
|
|
20330
|
+
const [row] = await this.db.insert(this.usersTable).values(payload).returning();
|
|
21082
20331
|
return this.mapRowToUser(row);
|
|
21083
20332
|
}
|
|
21084
20333
|
async getUserById(id) {
|
|
@@ -21117,12 +20366,12 @@ var UserService = class {
|
|
|
21117
20366
|
}));
|
|
21118
20367
|
}
|
|
21119
20368
|
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
21120
|
-
await this.
|
|
20369
|
+
await this.db.insert(this.userIdentitiesTable).values({
|
|
21121
20370
|
userId,
|
|
21122
20371
|
provider,
|
|
21123
20372
|
providerId,
|
|
21124
20373
|
profileData: profileData || null
|
|
21125
|
-
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] })
|
|
20374
|
+
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
|
|
21126
20375
|
}
|
|
21127
20376
|
async updateUser(id, data) {
|
|
21128
20377
|
const idCol = getColumn(this.usersTable, "id");
|
|
@@ -21130,13 +20379,13 @@ var UserService = class {
|
|
|
21130
20379
|
const payload = this.mapPayload(data);
|
|
21131
20380
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21132
20381
|
payload[updatedAtKey] = /* @__PURE__ */ new Date();
|
|
21133
|
-
const [row] = await this.
|
|
20382
|
+
const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
|
|
21134
20383
|
return row ? this.mapRowToUser(row) : null;
|
|
21135
20384
|
}
|
|
21136
20385
|
async deleteUser(id) {
|
|
21137
20386
|
const idCol = getColumn(this.usersTable, "id");
|
|
21138
20387
|
if (!idCol) return;
|
|
21139
|
-
await this.
|
|
20388
|
+
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
21140
20389
|
}
|
|
21141
20390
|
async listUsers() {
|
|
21142
20391
|
return (await this.db.select().from(this.usersTable)).map((row) => this.mapRowToUser(row));
|
|
@@ -21190,10 +20439,10 @@ var UserService = class {
|
|
|
21190
20439
|
if (!idCol) return;
|
|
21191
20440
|
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
21192
20441
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21193
|
-
await this.
|
|
20442
|
+
await this.db.update(this.usersTable).set({
|
|
21194
20443
|
[passwordHashColKey]: passwordHash,
|
|
21195
20444
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21196
|
-
}).where(eq(idCol, id))
|
|
20445
|
+
}).where(eq(idCol, id));
|
|
21197
20446
|
}
|
|
21198
20447
|
/**
|
|
21199
20448
|
* Set email verification status
|
|
@@ -21204,11 +20453,11 @@ var UserService = class {
|
|
|
21204
20453
|
const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
21205
20454
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21206
20455
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21207
|
-
await this.
|
|
20456
|
+
await this.db.update(this.usersTable).set({
|
|
21208
20457
|
[emailVerifiedColKey]: verified,
|
|
21209
20458
|
[emailVerificationTokenColKey]: null,
|
|
21210
20459
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21211
|
-
}).where(eq(idCol, id))
|
|
20460
|
+
}).where(eq(idCol, id));
|
|
21212
20461
|
}
|
|
21213
20462
|
/**
|
|
21214
20463
|
* Set email verification token
|
|
@@ -21219,11 +20468,11 @@ var UserService = class {
|
|
|
21219
20468
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21220
20469
|
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
21221
20470
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21222
|
-
await this.
|
|
20471
|
+
await this.db.update(this.usersTable).set({
|
|
21223
20472
|
[emailVerificationTokenColKey]: token,
|
|
21224
20473
|
[emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
|
|
21225
20474
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21226
|
-
}).where(eq(idCol, id))
|
|
20475
|
+
}).where(eq(idCol, id));
|
|
21227
20476
|
}
|
|
21228
20477
|
/**
|
|
21229
20478
|
* Find user by email verification token
|
|
@@ -21268,22 +20517,22 @@ var UserService = class {
|
|
|
21268
20517
|
async setUserRoles(userId, roleIds) {
|
|
21269
20518
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21270
20519
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
21271
|
-
await this.
|
|
20520
|
+
await this.db.execute(sql`
|
|
21272
20521
|
UPDATE ${sql.raw(usersTableName)}
|
|
21273
20522
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
21274
20523
|
WHERE id = ${userId}
|
|
21275
|
-
`)
|
|
20524
|
+
`);
|
|
21276
20525
|
}
|
|
21277
20526
|
/**
|
|
21278
20527
|
* Assign a specific role to new user (appends if not present)
|
|
21279
20528
|
*/
|
|
21280
20529
|
async assignDefaultRole(userId, roleId) {
|
|
21281
20530
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21282
|
-
await this.
|
|
20531
|
+
await this.db.execute(sql`
|
|
21283
20532
|
UPDATE ${sql.raw(usersTableName)}
|
|
21284
20533
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
21285
20534
|
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
21286
|
-
`)
|
|
20535
|
+
`);
|
|
21287
20536
|
}
|
|
21288
20537
|
/**
|
|
21289
20538
|
* Get user with their roles
|
|
@@ -22667,14 +21916,21 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22667
21916
|
logger.info(`🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema "${pgSchemaName}" [${introspectedCollections.map((c) => c.slug).join(", ")}]`);
|
|
22668
21917
|
}
|
|
22669
21918
|
const activeCollections = introspectedCollections ?? collections;
|
|
21919
|
+
const registry = new PostgresCollectionRegistry();
|
|
21920
|
+
if (activeCollections) {
|
|
21921
|
+
registry.registerMultiple(activeCollections);
|
|
21922
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
21923
|
+
}
|
|
22670
21924
|
const schemaTables = introspectedTables ?? pgConfig.schema?.tables;
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
22675
|
-
|
|
22676
|
-
relations: schemaRelations
|
|
21925
|
+
if (schemaTables) Object.values(schemaTables).forEach((table) => {
|
|
21926
|
+
if (isTable(table)) {
|
|
21927
|
+
const tableName = getTableName(table);
|
|
21928
|
+
registry.registerTable(table, tableName);
|
|
21929
|
+
}
|
|
22677
21930
|
});
|
|
21931
|
+
if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums);
|
|
21932
|
+
const schemaRelations = introspectedRelations ?? pgConfig.schema?.relations;
|
|
21933
|
+
if (schemaRelations) registry.registerRelations(schemaRelations);
|
|
22678
21934
|
if (schemaTables) patchPgArrayNullSafety(schemaTables);
|
|
22679
21935
|
const mergedSchema = {
|
|
22680
21936
|
...schemaTables,
|
|
@@ -22753,7 +22009,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22753
22009
|
const wantsCdc = cdcMode !== "off";
|
|
22754
22010
|
const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
|
|
22755
22011
|
let cdcEnabled = false;
|
|
22756
|
-
let provisionCdcForTables;
|
|
22757
22012
|
if (wantsCdc && !directUrl) {
|
|
22758
22013
|
const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
|
|
22759
22014
|
if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
|
|
@@ -22770,9 +22025,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22770
22025
|
})).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
|
|
22771
22026
|
await realtimeService.enableCdc(directUrl);
|
|
22772
22027
|
cdcEnabled = true;
|
|
22773
|
-
provisionCdcForTables = async (tables) => {
|
|
22774
|
-
await provisionTriggerCdc(cdcRunSql, tables);
|
|
22775
|
-
};
|
|
22776
22028
|
logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.`);
|
|
22777
22029
|
} catch (err) {
|
|
22778
22030
|
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
|
|
@@ -22797,14 +22049,13 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22797
22049
|
const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
|
|
22798
22050
|
const missing = [];
|
|
22799
22051
|
for (const col of registeredCollections) {
|
|
22800
|
-
if (col.auth?.enabled) continue;
|
|
22801
22052
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
22802
22053
|
const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
|
|
22803
22054
|
const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
|
|
22804
22055
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
22805
22056
|
if (!dbTables.has(fullCheckName)) missing.push({
|
|
22806
22057
|
slug: col.slug,
|
|
22807
|
-
table:
|
|
22058
|
+
table: checkName
|
|
22808
22059
|
});
|
|
22809
22060
|
}
|
|
22810
22061
|
if (missing.length > 0) {
|
|
@@ -22838,8 +22089,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22838
22089
|
registry,
|
|
22839
22090
|
realtimeService,
|
|
22840
22091
|
driver,
|
|
22841
|
-
poolManager
|
|
22842
|
-
provisionCdcForTables
|
|
22092
|
+
poolManager
|
|
22843
22093
|
}
|
|
22844
22094
|
};
|
|
22845
22095
|
},
|
|
@@ -22851,18 +22101,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22851
22101
|
const registry = internals.registry;
|
|
22852
22102
|
const authCollection = authConfig.collection;
|
|
22853
22103
|
await ensureAuthTablesExist(db, authCollection);
|
|
22854
|
-
if (authCollection && internals.provisionCdcForTables) {
|
|
22855
|
-
const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
|
|
22856
|
-
const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
|
|
22857
|
-
if (authTable) try {
|
|
22858
|
-
await internals.provisionCdcForTables([{
|
|
22859
|
-
schema: authSchema,
|
|
22860
|
-
table: authTable
|
|
22861
|
-
}]);
|
|
22862
|
-
} catch (err) {
|
|
22863
|
-
logger.warn(`⚠️ [CDC] Could not attach change-capture to the auth table "${authSchema}.${authTable}" — writes to it won't emit database-level events.`, { detail: err instanceof Error ? err.message : String(err) });
|
|
22864
|
-
}
|
|
22865
|
-
}
|
|
22866
22104
|
let emailService;
|
|
22867
22105
|
if (authConfig.email) emailService = createEmailService(authConfig.email);
|
|
22868
22106
|
const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
|
|
@@ -22933,6 +22171,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
22933
22171
|
};
|
|
22934
22172
|
}
|
|
22935
22173
|
//#endregion
|
|
22936
|
-
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor,
|
|
22174
|
+
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveConnectionString, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, withDatabaseName };
|
|
22937
22175
|
|
|
22938
22176
|
//# sourceMappingURL=index.es.js.map
|