@rebasepro/server-postgres 0.9.1-canary.c0d4c07 → 0.9.1-canary.d198c11
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/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 +318 -619
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/security/policy-drift.d.ts +0 -30
- package/dist/services/FetchService.d.ts +32 -4
- package/dist/services/RelationService.d.ts +1 -34
- package/dist/services/collection-helpers.d.ts +0 -76
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +0 -7
- package/package.json +7 -11
- package/src/PostgresBackendDriver.ts +11 -39
- package/src/PostgresBootstrapper.ts +25 -62
- package/src/auth/ensure-tables.ts +11 -73
- package/src/auth/services.ts +19 -49
- package/src/cli.ts +0 -60
- package/src/connection.ts +1 -61
- package/src/data-transformer.ts +9 -11
- package/src/databasePoolManager.ts +0 -2
- package/src/schema/doctor.ts +20 -45
- package/src/schema/introspect-db.ts +2 -19
- package/src/security/policy-drift.test.ts +1 -106
- package/src/security/policy-drift.ts +0 -56
- package/src/services/FetchService.ts +229 -50
- package/src/services/PersistService.ts +2 -9
- package/src/services/RelationService.ts +94 -153
- package/src/services/collection-helpers.ts +3 -166
- package/src/services/index.ts +0 -1
- package/src/services/realtimeService.ts +19 -40
- package/src/utils/drizzle-conditions.ts +0 -13
- 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 -239
package/dist/index.es.js
CHANGED
|
@@ -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,
|
|
@@ -1846,17 +1808,12 @@ function parseIdValues(idValue, primaryKeys) {
|
|
|
1846
1808
|
/**
|
|
1847
1809
|
* The primary keys of a collection, as declared by its properties.
|
|
1848
1810
|
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1854
|
-
*
|
|
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.
|
|
1811
|
+
* The driver can also infer keys from the Drizzle schema, which the browser
|
|
1812
|
+
* cannot see — so the server normalizes what it resolved onto the config it
|
|
1813
|
+
* serves (see `stampPrimaryKeys`), and this reads that back. Returns an empty
|
|
1814
|
+
* array when a collection declares none, which callers must treat as "not
|
|
1815
|
+
* addressable" rather than defaulting to `id`: guessing a key that is not the
|
|
1816
|
+
* real one produces confidently wrong addresses.
|
|
1860
1817
|
*/
|
|
1861
1818
|
function getDeclaredPrimaryKeys(collection) {
|
|
1862
1819
|
const properties = collection.properties;
|
|
@@ -1881,15 +1838,9 @@ function getDeclaredPrimaryKeys(collection) {
|
|
|
1881
1838
|
* The postgres driver tries, in order: properties marked `isId`; the primary
|
|
1882
1839
|
* keys of the Drizzle schema; and finally a column literally named `id`. Only
|
|
1883
1840
|
* the first and last are visible in a `CollectionConfig`, which is what both
|
|
1884
|
-
* sides share
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
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.
|
|
1841
|
+
* sides share — so a collection whose key is *only* known to Drizzle, and is
|
|
1842
|
+
* not named `id`, resolves to nothing here. That is reported rather than
|
|
1843
|
+
* guessed: inventing a key produces addresses that look right and route wrong.
|
|
1893
1844
|
*/
|
|
1894
1845
|
function resolvePrimaryKeys(collection) {
|
|
1895
1846
|
const declared = getDeclaredPrimaryKeys(collection);
|
|
@@ -4144,7 +4095,7 @@ function createPrimaryKeyResolver(options) {
|
|
|
4144
4095
|
}
|
|
4145
4096
|
if (!warned.has(slug)) {
|
|
4146
4097
|
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
|
|
4098
|
+
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.`);
|
|
4148
4099
|
}
|
|
4149
4100
|
return keys;
|
|
4150
4101
|
};
|
|
@@ -4587,26 +4538,8 @@ function getTableForCollection(collection, registry) {
|
|
|
4587
4538
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4588
4539
|
return table;
|
|
4589
4540
|
}
|
|
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
4541
|
function getPrimaryKeys(collection, registry) {
|
|
4542
|
+
const table = getTableForCollection(collection, registry);
|
|
4610
4543
|
if (collection.properties) {
|
|
4611
4544
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4612
4545
|
fieldName: key,
|
|
@@ -4615,8 +4548,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4615
4548
|
}));
|
|
4616
4549
|
if (idProps.length > 0) return idProps;
|
|
4617
4550
|
}
|
|
4618
|
-
const table = registry.getTable(getTableName$1(collection));
|
|
4619
|
-
if (!table) return [];
|
|
4620
4551
|
const keys = [];
|
|
4621
4552
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4622
4553
|
const col = colRaw;
|
|
@@ -4644,90 +4575,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4644
4575
|
}
|
|
4645
4576
|
return keys;
|
|
4646
4577
|
}
|
|
4647
|
-
/**
|
|
4648
|
-
* The key columns, for callers that cannot do their job without one.
|
|
4649
|
-
*
|
|
4650
|
-
* {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a
|
|
4651
|
-
* collection with no address. Most of this driver, though, is building a WHERE
|
|
4652
|
-
* clause and has no meaning without a key — for those, an empty array is not an
|
|
4653
|
-
* answer, and indexing `[0]` into it produces `Cannot read properties of
|
|
4654
|
-
* undefined` three frames from where the real problem is. This says what is
|
|
4655
|
-
* wrong and which collection it is wrong about.
|
|
4656
|
-
*/
|
|
4657
|
-
function requirePrimaryKeys(collection, registry) {
|
|
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
|
-
});
|
|
4697
|
-
}
|
|
4698
|
-
return findings;
|
|
4699
|
-
}
|
|
4700
|
-
/**
|
|
4701
|
-
* Report the collections from {@link findUnresolvableKeyCollections} at boot,
|
|
4702
|
-
* with the edit that fixes each one.
|
|
4703
|
-
*
|
|
4704
|
-
* Grouped by failure, not by collection: the shadowed case is a routing bug and
|
|
4705
|
-
* the silent case is a missing feature, and they deserve different urgency.
|
|
4706
|
-
*/
|
|
4707
|
-
function warnOnKeysTheAdminCannotResolve(collections, registry) {
|
|
4708
|
-
const findings = findUnresolvableKeyCollections(collections, registry);
|
|
4709
|
-
if (findings.length === 0) return;
|
|
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");
|
|
4715
|
-
}
|
|
4716
|
-
/**
|
|
4717
|
-
* The address of a row: derived from the collection's primary keys, because a
|
|
4718
|
-
* row does not carry one — it is exactly its columns.
|
|
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 "";
|
|
4730
|
-
}
|
|
4731
4578
|
//#endregion
|
|
4732
4579
|
//#region src/utils/drizzle-conditions.ts
|
|
4733
4580
|
/**
|
|
@@ -5266,7 +5113,6 @@ var DrizzleConditionBuilder = class {
|
|
|
5266
5113
|
static buildVectorSearchConditions(table, vectorSearch) {
|
|
5267
5114
|
const column = table[vectorSearch.property];
|
|
5268
5115
|
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
5116
|
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
5271
5117
|
const distanceFn = vectorSearch.distance || "cosine";
|
|
5272
5118
|
let operator;
|
|
@@ -5350,10 +5196,12 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5350
5196
|
continue;
|
|
5351
5197
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
5352
5198
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
5199
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
5353
5200
|
inverseRelationUpdates.push({
|
|
5354
5201
|
relationKey: key,
|
|
5355
5202
|
relation,
|
|
5356
|
-
newValue: serializedValue
|
|
5203
|
+
newValue: serializedValue,
|
|
5204
|
+
currentId: row.id || buildCompositeId(row, pks)
|
|
5357
5205
|
});
|
|
5358
5206
|
continue;
|
|
5359
5207
|
} else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
@@ -5363,11 +5211,15 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5363
5211
|
relation,
|
|
5364
5212
|
newTargetId: serializedValue
|
|
5365
5213
|
});
|
|
5366
|
-
else
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5214
|
+
else {
|
|
5215
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
5216
|
+
inverseRelationUpdates.push({
|
|
5217
|
+
relationKey: key,
|
|
5218
|
+
relation,
|
|
5219
|
+
newValue: serializedValue,
|
|
5220
|
+
currentId: row.id || buildCompositeId(row, pks)
|
|
5221
|
+
});
|
|
5222
|
+
}
|
|
5371
5223
|
continue;
|
|
5372
5224
|
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
5373
5225
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
@@ -5767,63 +5619,6 @@ var RelationService = class {
|
|
|
5767
5619
|
this.registry = registry;
|
|
5768
5620
|
}
|
|
5769
5621
|
/**
|
|
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
5622
|
* Fetch rows related to a parent row through a specific relation
|
|
5828
5623
|
*/
|
|
5829
5624
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5842,9 +5637,9 @@ var RelationService = class {
|
|
|
5842
5637
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5843
5638
|
const targetCollection = relation.target();
|
|
5844
5639
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5845
|
-
const idInfo =
|
|
5640
|
+
const idInfo = getPrimaryKeys(targetCollection, this.registry);
|
|
5846
5641
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5847
|
-
const parentPks =
|
|
5642
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5848
5643
|
const parentIdInfo = parentPks[0];
|
|
5849
5644
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5850
5645
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5868,7 +5663,7 @@ var RelationService = class {
|
|
|
5868
5663
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5869
5664
|
currentTable = joinTable;
|
|
5870
5665
|
}
|
|
5871
|
-
const parentIdField = parentTable[
|
|
5666
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5872
5667
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5873
5668
|
if (options.limit) query = query.limit(options.limit);
|
|
5874
5669
|
const results = await query;
|
|
@@ -5876,7 +5671,13 @@ var RelationService = class {
|
|
|
5876
5671
|
const rows = [];
|
|
5877
5672
|
for (const row of results) {
|
|
5878
5673
|
const targetRow = row[targetTableName] || row;
|
|
5879
|
-
|
|
5674
|
+
const id = targetRow[idInfo[0].fieldName];
|
|
5675
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5676
|
+
rows.push({
|
|
5677
|
+
id: id?.toString() || "",
|
|
5678
|
+
path: targetCollection.slug,
|
|
5679
|
+
values: parsedValues
|
|
5680
|
+
});
|
|
5880
5681
|
}
|
|
5881
5682
|
return rows;
|
|
5882
5683
|
}
|
|
@@ -5895,7 +5696,13 @@ var RelationService = class {
|
|
|
5895
5696
|
const rows = [];
|
|
5896
5697
|
for (const row of results) {
|
|
5897
5698
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5898
|
-
|
|
5699
|
+
const id = targetRow[idInfo[0].fieldName];
|
|
5700
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5701
|
+
rows.push({
|
|
5702
|
+
id: id?.toString() || "",
|
|
5703
|
+
path: targetCollection.slug,
|
|
5704
|
+
values: parsedValues
|
|
5705
|
+
});
|
|
5899
5706
|
}
|
|
5900
5707
|
return rows;
|
|
5901
5708
|
}
|
|
@@ -5912,8 +5719,8 @@ var RelationService = class {
|
|
|
5912
5719
|
}
|
|
5913
5720
|
const targetCollection = relation.target();
|
|
5914
5721
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5915
|
-
const targetIdField = targetTable[
|
|
5916
|
-
const parentPks =
|
|
5722
|
+
const targetIdField = targetTable[getPrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5723
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5917
5724
|
const parentIdInfo = parentPks[0];
|
|
5918
5725
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5919
5726
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5932,10 +5739,9 @@ var RelationService = class {
|
|
|
5932
5739
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5933
5740
|
const targetCollection = relation.target();
|
|
5934
5741
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5935
|
-
const
|
|
5936
|
-
const targetIdInfo = targetPks[0];
|
|
5742
|
+
const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
|
|
5937
5743
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5938
|
-
const parentPks =
|
|
5744
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5939
5745
|
const parentIdInfo = parentPks[0];
|
|
5940
5746
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5941
5747
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5959,19 +5765,25 @@ var RelationService = class {
|
|
|
5959
5765
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5960
5766
|
currentTable = joinTable;
|
|
5961
5767
|
}
|
|
5962
|
-
|
|
5768
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5769
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5963
5770
|
const results = await query;
|
|
5964
5771
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5965
5772
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5966
5773
|
for (const row of results) {
|
|
5967
5774
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5968
5775
|
const targetRow = row[targetTableName] || row;
|
|
5969
|
-
|
|
5776
|
+
const parentId = parentRow[parentIdInfo.fieldName];
|
|
5777
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5778
|
+
resultMap.set(String(parentId), {
|
|
5779
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5780
|
+
path: targetCollection.slug,
|
|
5781
|
+
values: parsedValues
|
|
5782
|
+
});
|
|
5970
5783
|
}
|
|
5971
5784
|
return resultMap;
|
|
5972
5785
|
}
|
|
5973
5786
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5974
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5975
5787
|
const localKeyCol = parentTable[relation.localKey];
|
|
5976
5788
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5977
5789
|
const fkRows = await this.db.select({
|
|
@@ -6000,11 +5812,17 @@ var RelationService = class {
|
|
|
6000
5812
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6001
5813
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
6002
5814
|
const targetRow = targetById.get(String(fkValue));
|
|
6003
|
-
if (targetRow)
|
|
5815
|
+
if (targetRow) {
|
|
5816
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5817
|
+
resultMap.set(parentIdStr, {
|
|
5818
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5819
|
+
path: targetCollection.slug,
|
|
5820
|
+
values: parsedValues
|
|
5821
|
+
});
|
|
5822
|
+
}
|
|
6004
5823
|
}
|
|
6005
5824
|
return resultMap;
|
|
6006
5825
|
}
|
|
6007
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6008
5826
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6009
5827
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6010
5828
|
const results = await query;
|
|
@@ -6015,7 +5833,14 @@ var RelationService = class {
|
|
|
6015
5833
|
let parentId;
|
|
6016
5834
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6017
5835
|
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6018
|
-
if (parentId !== void 0 && parentIdSet.has(String(parentId)))
|
|
5836
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5837
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5838
|
+
resultMap.set(String(parentId), {
|
|
5839
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5840
|
+
path: targetCollection.slug,
|
|
5841
|
+
values: parsedValues
|
|
5842
|
+
});
|
|
5843
|
+
}
|
|
6019
5844
|
}
|
|
6020
5845
|
return resultMap;
|
|
6021
5846
|
}
|
|
@@ -6029,9 +5854,9 @@ var RelationService = class {
|
|
|
6029
5854
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
6030
5855
|
const targetCollection = relation.target();
|
|
6031
5856
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6032
|
-
const
|
|
6033
|
-
const targetIdField = targetTable[
|
|
6034
|
-
const parentPks =
|
|
5857
|
+
const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
|
|
5858
|
+
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5859
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6035
5860
|
const parentIdInfo = parentPks[0];
|
|
6036
5861
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
6037
5862
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -6053,22 +5878,27 @@ var RelationService = class {
|
|
|
6053
5878
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
6054
5879
|
currentTable = joinTable;
|
|
6055
5880
|
}
|
|
6056
|
-
|
|
5881
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5882
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6057
5883
|
const results = await query;
|
|
6058
5884
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
6059
5885
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6060
5886
|
for (const row of results) {
|
|
6061
5887
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
6062
5888
|
const targetRow = row[targetTableName] || row;
|
|
6063
|
-
const parentId =
|
|
5889
|
+
const parentId = String(parentRow[parentIdInfo.fieldName]);
|
|
5890
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6064
5891
|
const arr = resultMap.get(parentId) || [];
|
|
6065
|
-
arr.push(
|
|
5892
|
+
arr.push({
|
|
5893
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5894
|
+
path: targetCollection.slug,
|
|
5895
|
+
values: parsedValues
|
|
5896
|
+
});
|
|
6066
5897
|
resultMap.set(parentId, arr);
|
|
6067
5898
|
}
|
|
6068
5899
|
return resultMap;
|
|
6069
5900
|
}
|
|
6070
5901
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6071
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
6072
5902
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
6073
5903
|
if (!junctionTable) {
|
|
6074
5904
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -6087,13 +5917,17 @@ var RelationService = class {
|
|
|
6087
5917
|
const junctionData = row[relation.through.table] || row;
|
|
6088
5918
|
const targetData = row[targetTableName] || row;
|
|
6089
5919
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
5920
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
6090
5921
|
const arr = resultMap.get(parentId) || [];
|
|
6091
|
-
arr.push(
|
|
5922
|
+
arr.push({
|
|
5923
|
+
id: String(targetData[targetIdInfo.fieldName]),
|
|
5924
|
+
path: targetCollection.slug,
|
|
5925
|
+
values: parsedValues
|
|
5926
|
+
});
|
|
6092
5927
|
resultMap.set(parentId, arr);
|
|
6093
5928
|
}
|
|
6094
5929
|
return resultMap;
|
|
6095
5930
|
}
|
|
6096
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6097
5931
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6098
5932
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6099
5933
|
const results = await query;
|
|
@@ -6106,9 +5940,14 @@ var RelationService = class {
|
|
|
6106
5940
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6107
5941
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6108
5942
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5943
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6109
5944
|
const key = String(parentId);
|
|
6110
5945
|
const arr = resultMap.get(key) || [];
|
|
6111
|
-
arr.push(
|
|
5946
|
+
arr.push({
|
|
5947
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5948
|
+
path: targetCollection.slug,
|
|
5949
|
+
values: parsedValues
|
|
5950
|
+
});
|
|
6112
5951
|
resultMap.set(key, arr);
|
|
6113
5952
|
}
|
|
6114
5953
|
}
|
|
@@ -6156,12 +5995,12 @@ var RelationService = class {
|
|
|
6156
5995
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
6157
5996
|
continue;
|
|
6158
5997
|
}
|
|
6159
|
-
const parentPks =
|
|
5998
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6160
5999
|
const parentIdInfo = parentPks[0];
|
|
6161
6000
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6162
6001
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6163
6002
|
if (targetEntityIds.length > 0) {
|
|
6164
|
-
const targetPks =
|
|
6003
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6165
6004
|
const targetIdInfo = targetPks[0];
|
|
6166
6005
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6167
6006
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6181,12 +6020,12 @@ var RelationService = class {
|
|
|
6181
6020
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6182
6021
|
continue;
|
|
6183
6022
|
}
|
|
6184
|
-
const parentPks =
|
|
6023
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6185
6024
|
const parentIdInfo = parentPks[0];
|
|
6186
6025
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6187
6026
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6188
6027
|
if (targetEntityIds.length > 0) {
|
|
6189
|
-
const targetPks =
|
|
6028
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6190
6029
|
const targetIdInfo = targetPks[0];
|
|
6191
6030
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6192
6031
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6197,7 +6036,7 @@ var RelationService = class {
|
|
|
6197
6036
|
} 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
6037
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6199
6038
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6200
|
-
const targetPks =
|
|
6039
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6201
6040
|
const targetIdInfo = targetPks[0];
|
|
6202
6041
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6203
6042
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6205,7 +6044,7 @@ var RelationService = class {
|
|
|
6205
6044
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6206
6045
|
continue;
|
|
6207
6046
|
}
|
|
6208
|
-
const parentPks =
|
|
6047
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6209
6048
|
const parentIdInfo = parentPks[0];
|
|
6210
6049
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6211
6050
|
if (targetEntityIds.length > 0) {
|
|
@@ -6225,9 +6064,9 @@ var RelationService = class {
|
|
|
6225
6064
|
try {
|
|
6226
6065
|
const targetCollection = relation.target();
|
|
6227
6066
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6228
|
-
const targetPks =
|
|
6067
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6229
6068
|
const targetIdInfo = targetPks[0];
|
|
6230
|
-
const sourcePks =
|
|
6069
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6231
6070
|
const sourceIdInfo = sourcePks[0];
|
|
6232
6071
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6233
6072
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6308,12 +6147,12 @@ var RelationService = class {
|
|
|
6308
6147
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6309
6148
|
return;
|
|
6310
6149
|
}
|
|
6311
|
-
const sourcePks =
|
|
6150
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6312
6151
|
const sourceIdInfo = sourcePks[0];
|
|
6313
6152
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6314
6153
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6315
6154
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6316
|
-
const targetPks =
|
|
6155
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6317
6156
|
const targetIdInfo = targetPks[0];
|
|
6318
6157
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6319
6158
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6321,7 +6160,7 @@ var RelationService = class {
|
|
|
6321
6160
|
}));
|
|
6322
6161
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6323
6162
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6324
|
-
const targetPks =
|
|
6163
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6325
6164
|
const targetIdInfo = targetPks[0];
|
|
6326
6165
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6327
6166
|
const newLink = {
|
|
@@ -6352,12 +6191,12 @@ var RelationService = class {
|
|
|
6352
6191
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6353
6192
|
return;
|
|
6354
6193
|
}
|
|
6355
|
-
const sourcePks =
|
|
6194
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6356
6195
|
const sourceIdInfo = sourcePks[0];
|
|
6357
6196
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6358
6197
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6359
6198
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6360
|
-
const targetPks =
|
|
6199
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6361
6200
|
const targetIdInfo = targetPks[0];
|
|
6362
6201
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6363
6202
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6378,12 +6217,12 @@ var RelationService = class {
|
|
|
6378
6217
|
const { relation, newTargetId } = upd;
|
|
6379
6218
|
const targetCollection = relation.target();
|
|
6380
6219
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6381
|
-
const targetPks =
|
|
6220
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6382
6221
|
const targetIdInfo = targetPks[0];
|
|
6383
6222
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6384
6223
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6385
6224
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6386
|
-
const parentPks =
|
|
6225
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6387
6226
|
const parentIdInfo = parentPks[0];
|
|
6388
6227
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6389
6228
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6454,7 +6293,7 @@ var RelationService = class {
|
|
|
6454
6293
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6455
6294
|
return;
|
|
6456
6295
|
}
|
|
6457
|
-
const targetPks =
|
|
6296
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6458
6297
|
const targetIdInfo = targetPks[0];
|
|
6459
6298
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6460
6299
|
const junctionData = {
|
|
@@ -6470,150 +6309,6 @@ var RelationService = class {
|
|
|
6470
6309
|
}
|
|
6471
6310
|
};
|
|
6472
6311
|
//#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
|
-
/**
|
|
6528
|
-
* Drop every column the collection marked `excludeFromApi`.
|
|
6529
|
-
*
|
|
6530
|
-
* Password hashes and verification tokens have to be readable server-side but
|
|
6531
|
-
* must never reach a client — and "never" has to mean every exit from this
|
|
6532
|
-
* pipeline, including relation targets, or a secret leaks through whichever
|
|
6533
|
-
* path was overlooked. Keyed by both the property name and its column name,
|
|
6534
|
-
* since a row can arrive keyed either way depending on the caller.
|
|
6535
|
-
*/
|
|
6536
|
-
function stripExcluded(row, collection) {
|
|
6537
|
-
const properties = collection.properties;
|
|
6538
|
-
if (!properties) return row;
|
|
6539
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
6540
|
-
if (!property?.excludeFromApi) continue;
|
|
6541
|
-
delete row[key];
|
|
6542
|
-
if (property.columnName) delete row[property.columnName];
|
|
6543
|
-
}
|
|
6544
|
-
return row;
|
|
6545
|
-
}
|
|
6546
|
-
function renderTarget(targetRow, targetCollection, style, registry) {
|
|
6547
|
-
if (style === "inline") return stripExcluded(coerceDeclaredNumbers({ ...targetRow }, targetCollection), targetCollection);
|
|
6548
|
-
const address = relationTargetAddress(targetRow, targetCollection, registry);
|
|
6549
|
-
const path = targetCollection.slug;
|
|
6550
|
-
return createRelationRefWithData(address, path, {
|
|
6551
|
-
id: address,
|
|
6552
|
-
path,
|
|
6553
|
-
values: normalizeDbValues(targetRow, targetCollection)
|
|
6554
|
-
});
|
|
6555
|
-
}
|
|
6556
|
-
/**
|
|
6557
|
-
* The address a relation ref points at.
|
|
6558
|
-
*
|
|
6559
|
-
* The whole key, not its first column: a composite-keyed target addressed by
|
|
6560
|
-
* `tenant_id` alone points at every row that shares it. A target whose key
|
|
6561
|
-
* cannot be resolved at all used to throw here — reading `[0]` of an empty
|
|
6562
|
-
* array — taking down the parent's fetch over a relation it may not even have
|
|
6563
|
-
* asked for. The first column is a guess, but a ref that resolves to nothing
|
|
6564
|
-
* beats no rows at all.
|
|
6565
|
-
*/
|
|
6566
|
-
function relationTargetAddress(targetRow, targetCollection, registry) {
|
|
6567
|
-
const address = deriveRowAddress(targetRow, targetCollection, registry);
|
|
6568
|
-
if (address) return address;
|
|
6569
|
-
return String(targetRow[Object.keys(targetRow)[0]] ?? "");
|
|
6570
|
-
}
|
|
6571
|
-
/**
|
|
6572
|
-
* The row the admin renders: every column, with relations as references.
|
|
6573
|
-
*
|
|
6574
|
-
* Values are normalized (dates, numbers, NaN) because the admin's view-model
|
|
6575
|
-
* expects real types. The row's own address is *not* among the columns — it is
|
|
6576
|
-
* derived by the consumer from the collection's primary keys.
|
|
6577
|
-
*/
|
|
6578
|
-
function toCmsRow(row, collection, registry) {
|
|
6579
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6580
|
-
const normalized = normalizeDbValues(row, collection);
|
|
6581
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6582
|
-
const relData = row[relation.relationName || key];
|
|
6583
|
-
if (relData === void 0 || relData === null) continue;
|
|
6584
|
-
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6585
|
-
const targetCollection = relation.target();
|
|
6586
|
-
normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
|
|
6587
|
-
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
|
|
6588
|
-
}
|
|
6589
|
-
return stripExcluded(normalized, collection);
|
|
6590
|
-
}
|
|
6591
|
-
/**
|
|
6592
|
-
* The row REST serves: every column under its own name, with the value Postgres
|
|
6593
|
-
* returned, and relations inlined as the target's columns.
|
|
6594
|
-
*
|
|
6595
|
-
* Values are the ones the database returned, except where that contradicts the
|
|
6596
|
-
* declared type: a `number` property is served as a number (see
|
|
6597
|
-
* {@link coerceDeclaredNumber}). Dates stay as the database returned them —
|
|
6598
|
-
* JSON has its own opinions about dates that the admin's view-model does not
|
|
6599
|
-
* share.
|
|
6600
|
-
*
|
|
6601
|
-
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
6602
|
-
* the relations `include` asked for, so the row is the authority on which are
|
|
6603
|
-
* actually there.
|
|
6604
|
-
*/
|
|
6605
|
-
function toRestRow(row, collection, registry) {
|
|
6606
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6607
|
-
const flat = {};
|
|
6608
|
-
for (const [key, value] of Object.entries(row)) {
|
|
6609
|
-
const relation = findRelation(resolvedRelations, key);
|
|
6610
|
-
if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
|
|
6611
|
-
else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
|
|
6612
|
-
else flat[key] = coerceDeclaredNumber(value, collection.properties?.[key]);
|
|
6613
|
-
}
|
|
6614
|
-
return stripExcluded(flat, collection);
|
|
6615
|
-
}
|
|
6616
|
-
//#endregion
|
|
6617
6312
|
//#region src/services/FetchService.ts
|
|
6618
6313
|
/**
|
|
6619
6314
|
* Service for handling all row read operations.
|
|
@@ -6672,7 +6367,7 @@ var FetchService = class {
|
|
|
6672
6367
|
if (!shouldInclude(key)) continue;
|
|
6673
6368
|
const drizzleRelName = relation.relationName || key;
|
|
6674
6369
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6675
|
-
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
6370
|
+
if (relation.cardinality === "many" && this.isJunctionRelation(relation, collection)) {
|
|
6676
6371
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6677
6372
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6678
6373
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6681,6 +6376,14 @@ var FetchService = class {
|
|
|
6681
6376
|
return withConfig;
|
|
6682
6377
|
}
|
|
6683
6378
|
/**
|
|
6379
|
+
* Detect if a many-to-many relation uses a junction table in the Drizzle schema.
|
|
6380
|
+
*/
|
|
6381
|
+
isJunctionRelation(relation, _collection) {
|
|
6382
|
+
if (relation.through) return true;
|
|
6383
|
+
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6384
|
+
return false;
|
|
6385
|
+
}
|
|
6386
|
+
/**
|
|
6684
6387
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6685
6388
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6686
6389
|
*/
|
|
@@ -6689,6 +6392,54 @@ var FetchService = class {
|
|
|
6689
6392
|
return null;
|
|
6690
6393
|
}
|
|
6691
6394
|
/**
|
|
6395
|
+
* Convert a db.query result row (with nested relation objects) to a flat row.
|
|
6396
|
+
* Handles:
|
|
6397
|
+
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
6398
|
+
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
6399
|
+
* - Flattening junction-table many-to-many results
|
|
6400
|
+
*
|
|
6401
|
+
* The row's own address is not among them: it is derived by the consumer
|
|
6402
|
+
* from the collection's primary keys.
|
|
6403
|
+
*/
|
|
6404
|
+
drizzleResultToRow(row, collection) {
|
|
6405
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6406
|
+
const normalizedValues = normalizeDbValues(row, collection);
|
|
6407
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6408
|
+
const relData = row[relation.relationName || key];
|
|
6409
|
+
if (relData === void 0 || relData === null) continue;
|
|
6410
|
+
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6411
|
+
const targetCollection = relation.target();
|
|
6412
|
+
const targetPath = targetCollection.slug;
|
|
6413
|
+
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
6414
|
+
normalizedValues[key] = relData.map((item) => {
|
|
6415
|
+
let targetRow = item;
|
|
6416
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6417
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6418
|
+
if (nestedKey) targetRow = item[nestedKey];
|
|
6419
|
+
}
|
|
6420
|
+
const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
|
|
6421
|
+
return createRelationRefWithData(relId, targetPath, {
|
|
6422
|
+
id: relId,
|
|
6423
|
+
path: targetPath,
|
|
6424
|
+
values: normalizeDbValues(targetRow, targetCollection)
|
|
6425
|
+
});
|
|
6426
|
+
});
|
|
6427
|
+
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
6428
|
+
const targetCollection = relation.target();
|
|
6429
|
+
const targetPath = targetCollection.slug;
|
|
6430
|
+
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
6431
|
+
const relObj = relData;
|
|
6432
|
+
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
6433
|
+
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
6434
|
+
id: relId,
|
|
6435
|
+
path: targetPath,
|
|
6436
|
+
values: normalizeDbValues(relObj, targetCollection)
|
|
6437
|
+
});
|
|
6438
|
+
}
|
|
6439
|
+
}
|
|
6440
|
+
return normalizedValues;
|
|
6441
|
+
}
|
|
6442
|
+
/**
|
|
6692
6443
|
* Post-fetch joinPath relations for a single flat row.
|
|
6693
6444
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6694
6445
|
* so they must be loaded separately after the primary query.
|
|
@@ -6709,6 +6460,31 @@ var FetchService = class {
|
|
|
6709
6460
|
await Promise.all(promises);
|
|
6710
6461
|
}
|
|
6711
6462
|
/**
|
|
6463
|
+
* Post-fetch joinPath relations for a batch of flat rows.
|
|
6464
|
+
* Uses batch fetching to avoid N+1 queries for list views.
|
|
6465
|
+
*/
|
|
6466
|
+
async resolveJoinPathRelationsBatch(rows, collection, collectionPath, idInfo, _databaseId) {
|
|
6467
|
+
if (rows.length === 0) return;
|
|
6468
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6469
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
6470
|
+
if (joinPathRelations.length === 0) return;
|
|
6471
|
+
for (const [key, relation] of joinPathRelations) try {
|
|
6472
|
+
const rowIds = rows.map((r) => {
|
|
6473
|
+
return parseIdValues(String(r.id), [idInfo])[idInfo.fieldName];
|
|
6474
|
+
});
|
|
6475
|
+
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6476
|
+
for (const row of rows) {
|
|
6477
|
+
const id = parseIdValues(String(row.id), [idInfo])[idInfo.fieldName];
|
|
6478
|
+
const relatedRow = resultMap.get(String(id));
|
|
6479
|
+
if (relatedRow) {
|
|
6480
|
+
if (relation.cardinality === "one") row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
} catch (e) {
|
|
6484
|
+
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
6485
|
+
}
|
|
6486
|
+
}
|
|
6487
|
+
/**
|
|
6712
6488
|
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
6713
6489
|
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
6714
6490
|
*/
|
|
@@ -6719,29 +6495,63 @@ var FetchService = class {
|
|
|
6719
6495
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6720
6496
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6721
6497
|
if (joinPathRelations.length === 0) return;
|
|
6722
|
-
const
|
|
6723
|
-
const address = buildCompositeId(row, idInfoArray);
|
|
6724
|
-
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6725
|
-
};
|
|
6498
|
+
const idInfo = idInfoArray[0];
|
|
6726
6499
|
for (const [key, relation] of joinPathRelations) try {
|
|
6727
|
-
const
|
|
6728
|
-
|
|
6729
|
-
|
|
6500
|
+
const rowIds = rows.map((r) => {
|
|
6501
|
+
return parseIdValues(String(r.id), idInfoArray)[idInfo.fieldName];
|
|
6502
|
+
});
|
|
6730
6503
|
if (relation.cardinality === "one") {
|
|
6731
6504
|
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6732
|
-
for (const row of
|
|
6733
|
-
const
|
|
6734
|
-
|
|
6505
|
+
for (const row of rows) {
|
|
6506
|
+
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
6507
|
+
const relatedRow = resultMap.get(String(id));
|
|
6508
|
+
if (relatedRow) row[key] = {
|
|
6509
|
+
...relatedRow.values,
|
|
6510
|
+
id: relatedRow.id
|
|
6511
|
+
};
|
|
6512
|
+
else row[key] = null;
|
|
6735
6513
|
}
|
|
6736
6514
|
} else if (relation.cardinality === "many") {
|
|
6737
6515
|
const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, rowIds, key, relation);
|
|
6738
|
-
for (const row of
|
|
6516
|
+
for (const row of rows) {
|
|
6517
|
+
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
6518
|
+
row[key] = (resultMap.get(String(id)) || []).map((e) => ({
|
|
6519
|
+
...e.values,
|
|
6520
|
+
id: e.id
|
|
6521
|
+
}));
|
|
6522
|
+
}
|
|
6739
6523
|
}
|
|
6740
6524
|
} catch (e) {
|
|
6741
6525
|
logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
|
|
6742
6526
|
}
|
|
6743
6527
|
}
|
|
6744
6528
|
/**
|
|
6529
|
+
* Convert a db.query result row to a flat REST-style row with populated relations.
|
|
6530
|
+
*
|
|
6531
|
+
* Every column is copied through under its own name, with the value Postgres
|
|
6532
|
+
* returned. This used to open with a synthesized `id` and then skip the key
|
|
6533
|
+
* column, which renamed it (a `sku` primary key was served as `id`, and `sku`
|
|
6534
|
+
* did not appear at all) and restringified it (`42` → `"42"`). Consumers that
|
|
6535
|
+
* need an address derive it from the collection's primary keys.
|
|
6536
|
+
*/
|
|
6537
|
+
drizzleResultToRestRow(row, collection) {
|
|
6538
|
+
const flat = {};
|
|
6539
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6540
|
+
for (const [k, v] of Object.entries(row)) {
|
|
6541
|
+
const relation = findRelation(resolvedRelations, k);
|
|
6542
|
+
if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
|
|
6543
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6544
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6545
|
+
if (nestedKey) return { ...item[nestedKey] };
|
|
6546
|
+
}
|
|
6547
|
+
return { ...item };
|
|
6548
|
+
});
|
|
6549
|
+
else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) flat[k] = { ...v };
|
|
6550
|
+
else flat[k] = v;
|
|
6551
|
+
}
|
|
6552
|
+
return flat;
|
|
6553
|
+
}
|
|
6554
|
+
/**
|
|
6745
6555
|
* Build db.query-compatible options from standard fetch options.
|
|
6746
6556
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6747
6557
|
*/
|
|
@@ -6811,7 +6621,7 @@ var FetchService = class {
|
|
|
6811
6621
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6812
6622
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6813
6623
|
const table = getTableForCollection(collection, this.registry);
|
|
6814
|
-
const idInfoArray =
|
|
6624
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6815
6625
|
const idInfo = idInfoArray[0];
|
|
6816
6626
|
const idField = table[idInfo.fieldName];
|
|
6817
6627
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6825,7 +6635,7 @@ var FetchService = class {
|
|
|
6825
6635
|
with: withConfig
|
|
6826
6636
|
});
|
|
6827
6637
|
if (!row) return void 0;
|
|
6828
|
-
const flatRow =
|
|
6638
|
+
const flatRow = this.drizzleResultToRow(row, collection);
|
|
6829
6639
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6830
6640
|
return flatRow;
|
|
6831
6641
|
} catch (e) {
|
|
@@ -6867,7 +6677,7 @@ var FetchService = class {
|
|
|
6867
6677
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6868
6678
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6869
6679
|
const table = getTableForCollection(collection, this.registry);
|
|
6870
|
-
const idInfoArray =
|
|
6680
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6871
6681
|
const idInfo = idInfoArray[0];
|
|
6872
6682
|
const idField = table[idInfo.fieldName];
|
|
6873
6683
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6877,7 +6687,7 @@ var FetchService = class {
|
|
|
6877
6687
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6878
6688
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6879
6689
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6880
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6690
|
+
return (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRow(row, collection));
|
|
6881
6691
|
} catch (e) {
|
|
6882
6692
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6883
6693
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6938,10 +6748,8 @@ var FetchService = class {
|
|
|
6938
6748
|
}
|
|
6939
6749
|
/**
|
|
6940
6750
|
* Fallback path used when db.query is unavailable.
|
|
6941
|
-
*
|
|
6942
|
-
*
|
|
6943
|
-
* relations from what drizzle already nested — no query per row. This one
|
|
6944
|
-
* has no nesting to read, so it resolves relations itself, in batches.
|
|
6751
|
+
* The primary path uses drizzleResultToRow which handles relation
|
|
6752
|
+
* mapping without N+1 queries.
|
|
6945
6753
|
*
|
|
6946
6754
|
* Process raw database results into flat rows with relations.
|
|
6947
6755
|
*/
|
|
@@ -7021,7 +6829,10 @@ var FetchService = class {
|
|
|
7021
6829
|
const relationKey = pathSegments[i];
|
|
7022
6830
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
7023
6831
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
7024
|
-
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6832
|
+
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6833
|
+
...row.values,
|
|
6834
|
+
id: row.id
|
|
6835
|
+
}));
|
|
7025
6836
|
if (i + 1 < pathSegments.length) {
|
|
7026
6837
|
const nextEntityId = pathSegments[i + 1];
|
|
7027
6838
|
currentCollection = relation.target();
|
|
@@ -7083,7 +6894,7 @@ var FetchService = class {
|
|
|
7083
6894
|
if (value === void 0 || value === null) return true;
|
|
7084
6895
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7085
6896
|
const table = getTableForCollection(collection, this.registry);
|
|
7086
|
-
const idInfoArray =
|
|
6897
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7087
6898
|
const idInfo = idInfoArray[0];
|
|
7088
6899
|
const idField = table[idInfo.fieldName];
|
|
7089
6900
|
const field = table[fieldName];
|
|
@@ -7110,7 +6921,7 @@ var FetchService = class {
|
|
|
7110
6921
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
7111
6922
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7112
6923
|
const table = getTableForCollection(collection, this.registry);
|
|
7113
|
-
const idInfoArray =
|
|
6924
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7114
6925
|
const idInfo = idInfoArray[0];
|
|
7115
6926
|
const idField = table[idInfo.fieldName];
|
|
7116
6927
|
const tableName = getTableName(table);
|
|
@@ -7118,7 +6929,7 @@ var FetchService = class {
|
|
|
7118
6929
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
7119
6930
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
7120
6931
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
7121
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
6932
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRestRow(row, collection));
|
|
7122
6933
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
7123
6934
|
return restRows;
|
|
7124
6935
|
} catch (e) {
|
|
@@ -7167,7 +6978,7 @@ var FetchService = class {
|
|
|
7167
6978
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
7168
6979
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7169
6980
|
const table = getTableForCollection(collection, this.registry);
|
|
7170
|
-
const idInfoArray =
|
|
6981
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7171
6982
|
const idInfo = idInfoArray[0];
|
|
7172
6983
|
const idField = table[idInfo.fieldName];
|
|
7173
6984
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -7180,7 +6991,7 @@ var FetchService = class {
|
|
|
7180
6991
|
...withConfig ? { with: withConfig } : {}
|
|
7181
6992
|
});
|
|
7182
6993
|
if (!row) return null;
|
|
7183
|
-
const restRow =
|
|
6994
|
+
const restRow = this.drizzleResultToRestRow(row, collection);
|
|
7184
6995
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
7185
6996
|
return restRow;
|
|
7186
6997
|
} catch (e) {
|
|
@@ -7225,7 +7036,7 @@ var FetchService = class {
|
|
|
7225
7036
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7226
7037
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7227
7038
|
const table = getTableForCollection(collection, this.registry);
|
|
7228
|
-
const idField = table[
|
|
7039
|
+
const idField = table[getPrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7229
7040
|
let vectorMeta;
|
|
7230
7041
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7231
7042
|
let query = vectorMeta ? this.db.select({
|
|
@@ -7714,7 +7525,7 @@ var PersistService = class {
|
|
|
7714
7525
|
} catch (error) {
|
|
7715
7526
|
throw this.toUserFriendlyError(error, collection.slug);
|
|
7716
7527
|
}
|
|
7717
|
-
const finalEntity = await this.fetchService.
|
|
7528
|
+
const finalEntity = await this.fetchService.fetchOne(collection.slug, savedId, databaseId);
|
|
7718
7529
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
7719
7530
|
return finalEntity;
|
|
7720
7531
|
}
|
|
@@ -8633,14 +8444,12 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8633
8444
|
let updatedValues = values;
|
|
8634
8445
|
const contextForCallback = this.buildCallContext();
|
|
8635
8446
|
let previousValuesForHistory;
|
|
8636
|
-
if (status === "existing" && id)
|
|
8637
|
-
const existing = await this.dataService.
|
|
8447
|
+
if (status === "existing" && id) {
|
|
8448
|
+
const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
|
|
8638
8449
|
if (existing) {
|
|
8639
8450
|
const { id: _existingId, ...existingValues } = existing;
|
|
8640
8451
|
previousValuesForHistory = existingValues;
|
|
8641
8452
|
}
|
|
8642
|
-
} catch (err) {
|
|
8643
|
-
logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
|
|
8644
8453
|
}
|
|
8645
8454
|
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
8646
8455
|
if (globalCallbacks?.beforeSave) {
|
|
@@ -8708,8 +8517,8 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8708
8517
|
context: contextForCallback
|
|
8709
8518
|
});
|
|
8710
8519
|
}
|
|
8711
|
-
const savedId =
|
|
8712
|
-
const savedValues = savedRow;
|
|
8520
|
+
const savedId = savedRow.id;
|
|
8521
|
+
const { id: _savedId, ...savedValues } = savedRow;
|
|
8713
8522
|
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
8714
8523
|
if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
|
|
8715
8524
|
collection: resolvedCollection,
|
|
@@ -8741,7 +8550,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8741
8550
|
}
|
|
8742
8551
|
if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
|
|
8743
8552
|
tableName: path,
|
|
8744
|
-
id: savedId,
|
|
8553
|
+
id: savedId.toString(),
|
|
8745
8554
|
action: status === "new" ? "create" : "update",
|
|
8746
8555
|
values: savedValues,
|
|
8747
8556
|
previousValues: previousValuesForHistory,
|
|
@@ -8749,11 +8558,11 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8749
8558
|
});
|
|
8750
8559
|
if (this._deferNotifications) this._pendingNotifications.push({
|
|
8751
8560
|
path,
|
|
8752
|
-
id: savedId,
|
|
8561
|
+
id: savedId.toString(),
|
|
8753
8562
|
row: savedRow,
|
|
8754
8563
|
databaseId: resolvedCollection?.databaseId
|
|
8755
8564
|
});
|
|
8756
|
-
else await this.realtimeService.notifyUpdate(path, savedId, savedRow, resolvedCollection?.databaseId);
|
|
8565
|
+
else await this.realtimeService.notifyUpdate(path, savedId.toString(), savedRow, resolvedCollection?.databaseId);
|
|
8757
8566
|
return savedRow;
|
|
8758
8567
|
} catch (error) {
|
|
8759
8568
|
if (globalCallbacks?.afterSaveError || callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
@@ -8833,7 +8642,10 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8833
8642
|
}
|
|
8834
8643
|
async delete({ row, collection }) {
|
|
8835
8644
|
const targetPath = row.path;
|
|
8836
|
-
const targetRow = {
|
|
8645
|
+
const targetRow = {
|
|
8646
|
+
id: row.id,
|
|
8647
|
+
...row.values ?? {}
|
|
8648
|
+
};
|
|
8837
8649
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, targetPath);
|
|
8838
8650
|
const contextForCallback = this.buildCallContext();
|
|
8839
8651
|
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
@@ -9253,7 +9065,6 @@ var DatabasePoolManager = class {
|
|
|
9253
9065
|
pool.on("error", (err) => {
|
|
9254
9066
|
logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
|
|
9255
9067
|
});
|
|
9256
|
-
guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
|
|
9257
9068
|
this.pools.set(databaseName, pool);
|
|
9258
9069
|
return pool;
|
|
9259
9070
|
}
|
|
@@ -10517,7 +10328,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10517
10328
|
startAfter: request.startAfter,
|
|
10518
10329
|
searchString: request.searchString
|
|
10519
10330
|
}, authContext);
|
|
10520
|
-
this.sendCollectionUpdate(clientId, subscriptionId, rows
|
|
10331
|
+
this.sendCollectionUpdate(clientId, subscriptionId, rows);
|
|
10521
10332
|
} catch (error) {
|
|
10522
10333
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
10523
10334
|
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10618,7 +10429,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10618
10429
|
if (subscription.type === "single" && notifyPath === originalPath) if (row && row?._rebase_invalidated) this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);
|
|
10619
10430
|
else this.sendSingleUpdate(subscription.clientId, subscriptionId, row);
|
|
10620
10431
|
else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
10621
|
-
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row
|
|
10432
|
+
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row);
|
|
10622
10433
|
this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
|
|
10623
10434
|
}
|
|
10624
10435
|
} catch (error) {
|
|
@@ -10648,7 +10459,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10648
10459
|
if (!this._subscriptions.has(subscriptionId)) return;
|
|
10649
10460
|
try {
|
|
10650
10461
|
const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
|
|
10651
|
-
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows
|
|
10462
|
+
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows);
|
|
10652
10463
|
} catch (error) {
|
|
10653
10464
|
const sanitized = sanitizeErrorForClient(error, notifyPath);
|
|
10654
10465
|
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10862,12 +10673,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10862
10673
|
}
|
|
10863
10674
|
return await this.dataService.fetchOne(notifyPath, id);
|
|
10864
10675
|
}
|
|
10865
|
-
sendCollectionUpdate(clientId, subscriptionId, rows
|
|
10676
|
+
sendCollectionUpdate(clientId, subscriptionId, rows) {
|
|
10866
10677
|
const message = {
|
|
10867
10678
|
type: "collection_update",
|
|
10868
10679
|
subscriptionId,
|
|
10869
|
-
rows
|
|
10870
|
-
pks: this.primaryKeysForPath(path)
|
|
10680
|
+
rows
|
|
10871
10681
|
};
|
|
10872
10682
|
this.sendMessage(clientId, message);
|
|
10873
10683
|
}
|
|
@@ -10882,33 +10692,16 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10882
10692
|
/**
|
|
10883
10693
|
* Send a lightweight row-level patch to a collection subscriber.
|
|
10884
10694
|
* The client can merge this into its cached data for instant feedback.
|
|
10885
|
-
*
|
|
10886
|
-
* The key columns ride along: the patch names a row by address, and the
|
|
10887
|
-
* client has to find that row among the ones it cached — which carry
|
|
10888
|
-
* columns and no address. The SDK holds no collection config to derive one
|
|
10889
|
-
* from, so this is the only place the mapping can come from.
|
|
10890
10695
|
*/
|
|
10891
|
-
sendCollectionPatch(clientId, subscriptionId, id, row
|
|
10696
|
+
sendCollectionPatch(clientId, subscriptionId, id, row) {
|
|
10892
10697
|
const message = {
|
|
10893
10698
|
type: "collection_patch",
|
|
10894
10699
|
subscriptionId,
|
|
10895
10700
|
id,
|
|
10896
|
-
row
|
|
10897
|
-
pks: this.primaryKeysForPath(notifyPath)
|
|
10701
|
+
row
|
|
10898
10702
|
};
|
|
10899
10703
|
this.sendMessage(clientId, message);
|
|
10900
10704
|
}
|
|
10901
|
-
/** The key columns of the collection at `path`, if they can be resolved. */
|
|
10902
|
-
primaryKeysForPath(path) {
|
|
10903
|
-
try {
|
|
10904
|
-
const collection = this.registry.getCollectionByPath(path);
|
|
10905
|
-
if (!collection) return void 0;
|
|
10906
|
-
const keys = getPrimaryKeys(collection, this.registry);
|
|
10907
|
-
return keys.length > 0 ? keys : void 0;
|
|
10908
|
-
} catch {
|
|
10909
|
-
return;
|
|
10910
|
-
}
|
|
10911
|
-
}
|
|
10912
10705
|
sendError(clientId, error, subscriptionId, code) {
|
|
10913
10706
|
const message = {
|
|
10914
10707
|
type: "error",
|
|
@@ -11156,7 +10949,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
11156
10949
|
}
|
|
11157
10950
|
/** Compute the canonical (possibly composite) id string from a captured row. */
|
|
11158
10951
|
extractIdFromCdcRow(collection, row) {
|
|
11159
|
-
|
|
10952
|
+
try {
|
|
10953
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, this.registry));
|
|
10954
|
+
if (composite && composite !== ":::") return composite;
|
|
10955
|
+
} catch {}
|
|
10956
|
+
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
10957
|
+
return "*";
|
|
11160
10958
|
}
|
|
11161
10959
|
dedupKey(path, id, databaseId) {
|
|
11162
10960
|
return `${databaseId ?? ""}::${path}::${id}`;
|
|
@@ -20616,33 +20414,6 @@ function formatBytes(bytes) {
|
|
|
20616
20414
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
20617
20415
|
}
|
|
20618
20416
|
//#endregion
|
|
20619
|
-
//#region src/collections/buildRegistry.ts
|
|
20620
|
-
/**
|
|
20621
|
-
* Build the collection registry for a driver.
|
|
20622
|
-
*
|
|
20623
|
-
* The order matters and is the reason this is one function rather than a run of
|
|
20624
|
-
* statements in the bootstrapper. Keys are resolved from the drizzle schema, so
|
|
20625
|
-
* anything that inspects them has to run *after* the tables are registered —
|
|
20626
|
-
* and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
|
|
20627
|
-
* collection whose table it cannot look up is one it has nothing to say about.
|
|
20628
|
-
* Warned too early, it would skip every collection and report nothing, which
|
|
20629
|
-
* reads exactly like having nothing to report.
|
|
20630
|
-
*/
|
|
20631
|
-
function buildCollectionRegistry(schema) {
|
|
20632
|
-
const registry = new PostgresCollectionRegistry();
|
|
20633
|
-
if (schema.collections) {
|
|
20634
|
-
registry.registerMultiple(schema.collections);
|
|
20635
|
-
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
20636
|
-
}
|
|
20637
|
-
if (schema.tables) Object.values(schema.tables).forEach((table) => {
|
|
20638
|
-
if (isTable(table)) registry.registerTable(table, getTableName(table));
|
|
20639
|
-
});
|
|
20640
|
-
if (schema.enums) registry.registerEnums(schema.enums);
|
|
20641
|
-
if (schema.relations) registry.registerRelations(schema.relations);
|
|
20642
|
-
warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
|
|
20643
|
-
return registry;
|
|
20644
|
-
}
|
|
20645
|
-
//#endregion
|
|
20646
20417
|
//#region src/auth/ensure-tables.ts
|
|
20647
20418
|
/**
|
|
20648
20419
|
* Auto-create auth tables if they don't exist.
|
|
@@ -20814,22 +20585,14 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20814
20585
|
$$ LANGUAGE sql STABLE
|
|
20815
20586
|
`);
|
|
20816
20587
|
});
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
20820
|
-
|
|
20821
|
-
|
|
20822
|
-
|
|
20823
|
-
|
|
20824
|
-
|
|
20825
|
-
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
20826
|
-
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
20827
|
-
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
|
|
20828
|
-
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
|
|
20829
|
-
]) await db.execute(sql`
|
|
20830
|
-
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20831
|
-
ADD COLUMN IF NOT EXISTS ${sql.raw(columnDef)}
|
|
20832
|
-
`);
|
|
20588
|
+
await db.execute(sql`
|
|
20589
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20590
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
20591
|
+
`);
|
|
20592
|
+
await db.execute(sql`
|
|
20593
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20594
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
20595
|
+
`);
|
|
20833
20596
|
try {
|
|
20834
20597
|
if ((await db.execute(sql`
|
|
20835
20598
|
SELECT EXISTS (
|
|
@@ -20900,34 +20663,6 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20900
20663
|
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
20901
20664
|
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
20902
20665
|
`);
|
|
20903
|
-
try {
|
|
20904
|
-
const authTablePairs = [
|
|
20905
|
-
[usersSchema, resolvedTable],
|
|
20906
|
-
[authSchema, "user_identities"],
|
|
20907
|
-
[authSchema, "refresh_tokens"],
|
|
20908
|
-
[authSchema, "password_reset_tokens"],
|
|
20909
|
-
[authSchema, "app_config"],
|
|
20910
|
-
[authSchema, "mfa_factors"],
|
|
20911
|
-
[authSchema, "mfa_challenges"],
|
|
20912
|
-
[authSchema, "recovery_codes"]
|
|
20913
|
-
];
|
|
20914
|
-
for (const [schemaName, tableName] of authTablePairs) if ((await db.execute(sql`
|
|
20915
|
-
SELECT 1
|
|
20916
|
-
FROM pg_class c
|
|
20917
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
20918
|
-
WHERE n.nspname = ${schemaName}
|
|
20919
|
-
AND c.relname = ${tableName}
|
|
20920
|
-
AND c.relforcerowsecurity
|
|
20921
|
-
`)).rows.length > 0) {
|
|
20922
|
-
await db.execute(sql`
|
|
20923
|
-
ALTER TABLE ${sql.raw(`"${schemaName}"."${tableName}"`)}
|
|
20924
|
-
NO FORCE ROW LEVEL SECURITY
|
|
20925
|
-
`);
|
|
20926
|
-
logger.warn(`🔧 Cleared stale FORCE ROW LEVEL SECURITY on "${schemaName}"."${tableName}" (legacy RLS model — it binds the owner connection and breaks privileged auth writes)`);
|
|
20927
|
-
}
|
|
20928
|
-
} catch (rlsReconcileError) {
|
|
20929
|
-
logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
|
|
20930
|
-
}
|
|
20931
20666
|
logger.info("✅ Auth tables ready");
|
|
20932
20667
|
} catch (error) {
|
|
20933
20668
|
logger.error("❌ Failed to create auth tables", { error });
|
|
@@ -20975,31 +20710,6 @@ var UserService = class {
|
|
|
20975
20710
|
const name = getTableName(this.usersTable);
|
|
20976
20711
|
return `"${getTableConfig(this.usersTable).schema || "public"}"."${name}"`;
|
|
20977
20712
|
}
|
|
20978
|
-
/**
|
|
20979
|
-
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
20980
|
-
*
|
|
20981
|
-
* The auth services run on the base/owner connection, which by design
|
|
20982
|
-
* carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
|
|
20983
|
-
* in the default policies applies. That NULL is normally guaranteed by
|
|
20984
|
-
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
20985
|
-
* GUC that survives on a pooled connection (or a connection role that
|
|
20986
|
-
* doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
|
|
20987
|
-
* turns the trusted write into an RLS-scoped one and denies it with
|
|
20988
|
-
* SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
|
|
20989
|
-
* single chokepoint, makes the server context deterministic instead of
|
|
20990
|
-
* trusting whatever state the pool hands us. `auth.uid()` reads '' as
|
|
20991
|
-
* NULL via NULLIF, so '' is the server context.
|
|
20992
|
-
*/
|
|
20993
|
-
async withServerContext(fn) {
|
|
20994
|
-
return await this.db.transaction(async (tx) => {
|
|
20995
|
-
await tx.execute(sql`
|
|
20996
|
-
SELECT set_config('app.user_id', '', true),
|
|
20997
|
-
set_config('app.user_roles', '', true),
|
|
20998
|
-
set_config('app.jwt', '', true)
|
|
20999
|
-
`);
|
|
21000
|
-
return await fn(tx);
|
|
21001
|
-
});
|
|
21002
|
-
}
|
|
21003
20713
|
mapRowToUser(row) {
|
|
21004
20714
|
if (!row) return row;
|
|
21005
20715
|
const id = row.id ?? row.uid;
|
|
@@ -21097,7 +20807,7 @@ var UserService = class {
|
|
|
21097
20807
|
}
|
|
21098
20808
|
async createUser(data) {
|
|
21099
20809
|
const payload = this.mapPayload(data);
|
|
21100
|
-
const [row] = await this.
|
|
20810
|
+
const [row] = await this.db.insert(this.usersTable).values(payload).returning();
|
|
21101
20811
|
return this.mapRowToUser(row);
|
|
21102
20812
|
}
|
|
21103
20813
|
async getUserById(id) {
|
|
@@ -21136,12 +20846,12 @@ var UserService = class {
|
|
|
21136
20846
|
}));
|
|
21137
20847
|
}
|
|
21138
20848
|
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
21139
|
-
await this.
|
|
20849
|
+
await this.db.insert(this.userIdentitiesTable).values({
|
|
21140
20850
|
userId,
|
|
21141
20851
|
provider,
|
|
21142
20852
|
providerId,
|
|
21143
20853
|
profileData: profileData || null
|
|
21144
|
-
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] })
|
|
20854
|
+
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
|
|
21145
20855
|
}
|
|
21146
20856
|
async updateUser(id, data) {
|
|
21147
20857
|
const idCol = getColumn(this.usersTable, "id");
|
|
@@ -21149,13 +20859,13 @@ var UserService = class {
|
|
|
21149
20859
|
const payload = this.mapPayload(data);
|
|
21150
20860
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21151
20861
|
payload[updatedAtKey] = /* @__PURE__ */ new Date();
|
|
21152
|
-
const [row] = await this.
|
|
20862
|
+
const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
|
|
21153
20863
|
return row ? this.mapRowToUser(row) : null;
|
|
21154
20864
|
}
|
|
21155
20865
|
async deleteUser(id) {
|
|
21156
20866
|
const idCol = getColumn(this.usersTable, "id");
|
|
21157
20867
|
if (!idCol) return;
|
|
21158
|
-
await this.
|
|
20868
|
+
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
21159
20869
|
}
|
|
21160
20870
|
async listUsers() {
|
|
21161
20871
|
return (await this.db.select().from(this.usersTable)).map((row) => this.mapRowToUser(row));
|
|
@@ -21209,10 +20919,10 @@ var UserService = class {
|
|
|
21209
20919
|
if (!idCol) return;
|
|
21210
20920
|
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
21211
20921
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21212
|
-
await this.
|
|
20922
|
+
await this.db.update(this.usersTable).set({
|
|
21213
20923
|
[passwordHashColKey]: passwordHash,
|
|
21214
20924
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21215
|
-
}).where(eq(idCol, id))
|
|
20925
|
+
}).where(eq(idCol, id));
|
|
21216
20926
|
}
|
|
21217
20927
|
/**
|
|
21218
20928
|
* Set email verification status
|
|
@@ -21223,11 +20933,11 @@ var UserService = class {
|
|
|
21223
20933
|
const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
21224
20934
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21225
20935
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21226
|
-
await this.
|
|
20936
|
+
await this.db.update(this.usersTable).set({
|
|
21227
20937
|
[emailVerifiedColKey]: verified,
|
|
21228
20938
|
[emailVerificationTokenColKey]: null,
|
|
21229
20939
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21230
|
-
}).where(eq(idCol, id))
|
|
20940
|
+
}).where(eq(idCol, id));
|
|
21231
20941
|
}
|
|
21232
20942
|
/**
|
|
21233
20943
|
* Set email verification token
|
|
@@ -21238,11 +20948,11 @@ var UserService = class {
|
|
|
21238
20948
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21239
20949
|
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
21240
20950
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21241
|
-
await this.
|
|
20951
|
+
await this.db.update(this.usersTable).set({
|
|
21242
20952
|
[emailVerificationTokenColKey]: token,
|
|
21243
20953
|
[emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
|
|
21244
20954
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21245
|
-
}).where(eq(idCol, id))
|
|
20955
|
+
}).where(eq(idCol, id));
|
|
21246
20956
|
}
|
|
21247
20957
|
/**
|
|
21248
20958
|
* Find user by email verification token
|
|
@@ -21287,22 +20997,22 @@ var UserService = class {
|
|
|
21287
20997
|
async setUserRoles(userId, roleIds) {
|
|
21288
20998
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21289
20999
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
21290
|
-
await this.
|
|
21000
|
+
await this.db.execute(sql`
|
|
21291
21001
|
UPDATE ${sql.raw(usersTableName)}
|
|
21292
21002
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
21293
21003
|
WHERE id = ${userId}
|
|
21294
|
-
`)
|
|
21004
|
+
`);
|
|
21295
21005
|
}
|
|
21296
21006
|
/**
|
|
21297
21007
|
* Assign a specific role to new user (appends if not present)
|
|
21298
21008
|
*/
|
|
21299
21009
|
async assignDefaultRole(userId, roleId) {
|
|
21300
21010
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21301
|
-
await this.
|
|
21011
|
+
await this.db.execute(sql`
|
|
21302
21012
|
UPDATE ${sql.raw(usersTableName)}
|
|
21303
21013
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
21304
21014
|
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
21305
|
-
`)
|
|
21015
|
+
`);
|
|
21306
21016
|
}
|
|
21307
21017
|
/**
|
|
21308
21018
|
* Get user with their roles
|
|
@@ -22686,14 +22396,21 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22686
22396
|
logger.info(`🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema "${pgSchemaName}" [${introspectedCollections.map((c) => c.slug).join(", ")}]`);
|
|
22687
22397
|
}
|
|
22688
22398
|
const activeCollections = introspectedCollections ?? collections;
|
|
22399
|
+
const registry = new PostgresCollectionRegistry();
|
|
22400
|
+
if (activeCollections) {
|
|
22401
|
+
registry.registerMultiple(activeCollections);
|
|
22402
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
22403
|
+
}
|
|
22689
22404
|
const schemaTables = introspectedTables ?? pgConfig.schema?.tables;
|
|
22690
|
-
|
|
22691
|
-
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
relations: schemaRelations
|
|
22405
|
+
if (schemaTables) Object.values(schemaTables).forEach((table) => {
|
|
22406
|
+
if (isTable(table)) {
|
|
22407
|
+
const tableName = getTableName(table);
|
|
22408
|
+
registry.registerTable(table, tableName);
|
|
22409
|
+
}
|
|
22696
22410
|
});
|
|
22411
|
+
if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums);
|
|
22412
|
+
const schemaRelations = introspectedRelations ?? pgConfig.schema?.relations;
|
|
22413
|
+
if (schemaRelations) registry.registerRelations(schemaRelations);
|
|
22697
22414
|
if (schemaTables) patchPgArrayNullSafety(schemaTables);
|
|
22698
22415
|
const mergedSchema = {
|
|
22699
22416
|
...schemaTables,
|
|
@@ -22772,7 +22489,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22772
22489
|
const wantsCdc = cdcMode !== "off";
|
|
22773
22490
|
const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
|
|
22774
22491
|
let cdcEnabled = false;
|
|
22775
|
-
let provisionCdcForTables;
|
|
22776
22492
|
if (wantsCdc && !directUrl) {
|
|
22777
22493
|
const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
|
|
22778
22494
|
if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
|
|
@@ -22789,9 +22505,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22789
22505
|
})).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
|
|
22790
22506
|
await realtimeService.enableCdc(directUrl);
|
|
22791
22507
|
cdcEnabled = true;
|
|
22792
|
-
provisionCdcForTables = async (tables) => {
|
|
22793
|
-
await provisionTriggerCdc(cdcRunSql, tables);
|
|
22794
|
-
};
|
|
22795
22508
|
logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.`);
|
|
22796
22509
|
} catch (err) {
|
|
22797
22510
|
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
|
|
@@ -22816,14 +22529,13 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22816
22529
|
const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
|
|
22817
22530
|
const missing = [];
|
|
22818
22531
|
for (const col of registeredCollections) {
|
|
22819
|
-
if (col.auth?.enabled) continue;
|
|
22820
22532
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
22821
22533
|
const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
|
|
22822
22534
|
const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
|
|
22823
22535
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
22824
22536
|
if (!dbTables.has(fullCheckName)) missing.push({
|
|
22825
22537
|
slug: col.slug,
|
|
22826
|
-
table:
|
|
22538
|
+
table: checkName
|
|
22827
22539
|
});
|
|
22828
22540
|
}
|
|
22829
22541
|
if (missing.length > 0) {
|
|
@@ -22857,8 +22569,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22857
22569
|
registry,
|
|
22858
22570
|
realtimeService,
|
|
22859
22571
|
driver,
|
|
22860
|
-
poolManager
|
|
22861
|
-
provisionCdcForTables
|
|
22572
|
+
poolManager
|
|
22862
22573
|
}
|
|
22863
22574
|
};
|
|
22864
22575
|
},
|
|
@@ -22870,18 +22581,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22870
22581
|
const registry = internals.registry;
|
|
22871
22582
|
const authCollection = authConfig.collection;
|
|
22872
22583
|
await ensureAuthTablesExist(db, authCollection);
|
|
22873
|
-
if (authCollection && internals.provisionCdcForTables) {
|
|
22874
|
-
const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
|
|
22875
|
-
const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
|
|
22876
|
-
if (authTable) try {
|
|
22877
|
-
await internals.provisionCdcForTables([{
|
|
22878
|
-
schema: authSchema,
|
|
22879
|
-
table: authTable
|
|
22880
|
-
}]);
|
|
22881
|
-
} catch (err) {
|
|
22882
|
-
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) });
|
|
22883
|
-
}
|
|
22884
|
-
}
|
|
22885
22584
|
let emailService;
|
|
22886
22585
|
if (authConfig.email) emailService = createEmailService(authConfig.email);
|
|
22887
22586
|
const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
|
|
@@ -22952,6 +22651,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
22952
22651
|
};
|
|
22953
22652
|
}
|
|
22954
22653
|
//#endregion
|
|
22955
|
-
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,
|
|
22654
|
+
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 };
|
|
22956
22655
|
|
|
22957
22656
|
//# sourceMappingURL=index.es.js.map
|