@rebasepro/server-postgres 0.9.1-canary.7ba0e49 → 0.9.1-canary.7dddf96
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/index.es.js +243 -436
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/services/FetchService.d.ts +38 -4
- package/dist/services/RelationService.d.ts +0 -34
- package/dist/services/collection-helpers.d.ts +7 -34
- package/package.json +7 -10
- package/src/PostgresBackendDriver.ts +5 -18
- package/src/PostgresBootstrapper.ts +2 -51
- package/src/auth/ensure-tables.ts +11 -73
- package/src/auth/services.ts +19 -49
- package/src/connection.ts +1 -61
- package/src/databasePoolManager.ts +0 -2
- package/src/schema/doctor.ts +20 -45
- package/src/schema/introspect-db.ts +2 -19
- package/src/services/FetchService.ts +163 -29
- package/src/services/PersistService.ts +2 -9
- package/src/services/RelationService.ts +93 -153
- package/src/services/collection-helpers.ts +27 -59
- package/src/services/realtimeService.ts +2 -4
- package/src/utils/drizzle-conditions.ts +0 -13
- package/dist/services/row-pipeline.d.ts +0 -63
- package/src/services/row-pipeline.ts +0 -215
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,
|
|
@@ -4587,26 +4549,8 @@ function getTableForCollection(collection, registry) {
|
|
|
4587
4549
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4588
4550
|
return table;
|
|
4589
4551
|
}
|
|
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
4552
|
function getPrimaryKeys(collection, registry) {
|
|
4553
|
+
const table = getTableForCollection(collection, registry);
|
|
4610
4554
|
if (collection.properties) {
|
|
4611
4555
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4612
4556
|
fieldName: key,
|
|
@@ -4615,8 +4559,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4615
4559
|
}));
|
|
4616
4560
|
if (idProps.length > 0) return idProps;
|
|
4617
4561
|
}
|
|
4618
|
-
const table = registry.getTable(getTableName$1(collection));
|
|
4619
|
-
if (!table) return [];
|
|
4620
4562
|
const keys = [];
|
|
4621
4563
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4622
4564
|
const col = colRaw;
|
|
@@ -4645,21 +4587,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4645
4587
|
return keys;
|
|
4646
4588
|
}
|
|
4647
4589
|
/**
|
|
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
4590
|
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
4664
4591
|
* instead.
|
|
4665
4592
|
*
|
|
@@ -4686,7 +4613,12 @@ function findUnresolvableKeyCollections(collections, registry) {
|
|
|
4686
4613
|
const findings = [];
|
|
4687
4614
|
for (const collection of collections) {
|
|
4688
4615
|
if (getDeclaredPrimaryKeys(collection).length > 0) continue;
|
|
4689
|
-
|
|
4616
|
+
let keys;
|
|
4617
|
+
try {
|
|
4618
|
+
keys = getPrimaryKeys(collection, registry);
|
|
4619
|
+
} catch {
|
|
4620
|
+
continue;
|
|
4621
|
+
}
|
|
4690
4622
|
if (keys.length === 0) continue;
|
|
4691
4623
|
if (keys.length === 1 && keys[0].fieldName === "id") continue;
|
|
4692
4624
|
findings.push({
|
|
@@ -4717,14 +4649,19 @@ function warnOnKeysTheAdminCannotResolve(collections, registry) {
|
|
|
4717
4649
|
* The address of a row: derived from the collection's primary keys, because a
|
|
4718
4650
|
* row does not carry one — it is exactly its columns.
|
|
4719
4651
|
*
|
|
4720
|
-
* Falls back to a literal `id` column,
|
|
4721
|
-
*
|
|
4722
|
-
*
|
|
4723
|
-
*
|
|
4652
|
+
* Falls back to a literal `id` column, which covers the two cases where the
|
|
4653
|
+
* keys cannot be resolved: a row that reached us from somewhere other than the
|
|
4654
|
+
* postgres driver, and a collection whose table the registry cannot look up
|
|
4655
|
+
* (`getPrimaryKeys` throws for an unregistered table rather than returning
|
|
4656
|
+
* nothing). Returns `""` when there is no key and no `id` — callers decide what
|
|
4657
|
+
* that means, since "unaddressable" is a different answer in a notification
|
|
4658
|
+
* (broadcast a wildcard) than in a save (fail).
|
|
4724
4659
|
*/
|
|
4725
4660
|
function deriveRowAddress(row, collection, registry) {
|
|
4726
|
-
|
|
4727
|
-
|
|
4661
|
+
try {
|
|
4662
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
|
|
4663
|
+
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4664
|
+
} catch {}
|
|
4728
4665
|
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
4729
4666
|
return "";
|
|
4730
4667
|
}
|
|
@@ -5266,7 +5203,6 @@ var DrizzleConditionBuilder = class {
|
|
|
5266
5203
|
static buildVectorSearchConditions(table, vectorSearch) {
|
|
5267
5204
|
const column = table[vectorSearch.property];
|
|
5268
5205
|
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
5206
|
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
5271
5207
|
const distanceFn = vectorSearch.distance || "cosine";
|
|
5272
5208
|
let operator;
|
|
@@ -5767,63 +5703,6 @@ var RelationService = class {
|
|
|
5767
5703
|
this.registry = registry;
|
|
5768
5704
|
}
|
|
5769
5705
|
/**
|
|
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
5706
|
* Fetch rows related to a parent row through a specific relation
|
|
5828
5707
|
*/
|
|
5829
5708
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5842,9 +5721,9 @@ var RelationService = class {
|
|
|
5842
5721
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5843
5722
|
const targetCollection = relation.target();
|
|
5844
5723
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5845
|
-
const idInfo =
|
|
5724
|
+
const idInfo = getPrimaryKeys(targetCollection, this.registry);
|
|
5846
5725
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5847
|
-
const parentPks =
|
|
5726
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5848
5727
|
const parentIdInfo = parentPks[0];
|
|
5849
5728
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5850
5729
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5868,7 +5747,7 @@ var RelationService = class {
|
|
|
5868
5747
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5869
5748
|
currentTable = joinTable;
|
|
5870
5749
|
}
|
|
5871
|
-
const parentIdField = parentTable[
|
|
5750
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5872
5751
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5873
5752
|
if (options.limit) query = query.limit(options.limit);
|
|
5874
5753
|
const results = await query;
|
|
@@ -5876,7 +5755,12 @@ var RelationService = class {
|
|
|
5876
5755
|
const rows = [];
|
|
5877
5756
|
for (const row of results) {
|
|
5878
5757
|
const targetRow = row[targetTableName] || row;
|
|
5879
|
-
|
|
5758
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5759
|
+
rows.push({
|
|
5760
|
+
id: buildCompositeId(targetRow, idInfo),
|
|
5761
|
+
path: targetCollection.slug,
|
|
5762
|
+
values: parsedValues
|
|
5763
|
+
});
|
|
5880
5764
|
}
|
|
5881
5765
|
return rows;
|
|
5882
5766
|
}
|
|
@@ -5895,7 +5779,12 @@ var RelationService = class {
|
|
|
5895
5779
|
const rows = [];
|
|
5896
5780
|
for (const row of results) {
|
|
5897
5781
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5898
|
-
|
|
5782
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5783
|
+
rows.push({
|
|
5784
|
+
id: buildCompositeId(targetRow, idInfo),
|
|
5785
|
+
path: targetCollection.slug,
|
|
5786
|
+
values: parsedValues
|
|
5787
|
+
});
|
|
5899
5788
|
}
|
|
5900
5789
|
return rows;
|
|
5901
5790
|
}
|
|
@@ -5912,8 +5801,8 @@ var RelationService = class {
|
|
|
5912
5801
|
}
|
|
5913
5802
|
const targetCollection = relation.target();
|
|
5914
5803
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5915
|
-
const targetIdField = targetTable[
|
|
5916
|
-
const parentPks =
|
|
5804
|
+
const targetIdField = targetTable[getPrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5805
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5917
5806
|
const parentIdInfo = parentPks[0];
|
|
5918
5807
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5919
5808
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5932,10 +5821,10 @@ var RelationService = class {
|
|
|
5932
5821
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5933
5822
|
const targetCollection = relation.target();
|
|
5934
5823
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5935
|
-
const targetPks =
|
|
5824
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
5936
5825
|
const targetIdInfo = targetPks[0];
|
|
5937
5826
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5938
|
-
const parentPks =
|
|
5827
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5939
5828
|
const parentIdInfo = parentPks[0];
|
|
5940
5829
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5941
5830
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5959,19 +5848,25 @@ var RelationService = class {
|
|
|
5959
5848
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5960
5849
|
currentTable = joinTable;
|
|
5961
5850
|
}
|
|
5962
|
-
|
|
5851
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5852
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5963
5853
|
const results = await query;
|
|
5964
5854
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5965
5855
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5966
5856
|
for (const row of results) {
|
|
5967
5857
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5968
5858
|
const targetRow = row[targetTableName] || row;
|
|
5969
|
-
|
|
5859
|
+
const parentId = parentRow[parentIdInfo.fieldName];
|
|
5860
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5861
|
+
resultMap.set(String(parentId), {
|
|
5862
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
5863
|
+
path: targetCollection.slug,
|
|
5864
|
+
values: parsedValues
|
|
5865
|
+
});
|
|
5970
5866
|
}
|
|
5971
5867
|
return resultMap;
|
|
5972
5868
|
}
|
|
5973
5869
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5974
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5975
5870
|
const localKeyCol = parentTable[relation.localKey];
|
|
5976
5871
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5977
5872
|
const fkRows = await this.db.select({
|
|
@@ -6000,11 +5895,17 @@ var RelationService = class {
|
|
|
6000
5895
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6001
5896
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
6002
5897
|
const targetRow = targetById.get(String(fkValue));
|
|
6003
|
-
if (targetRow)
|
|
5898
|
+
if (targetRow) {
|
|
5899
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5900
|
+
resultMap.set(parentIdStr, {
|
|
5901
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
5902
|
+
path: targetCollection.slug,
|
|
5903
|
+
values: parsedValues
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
6004
5906
|
}
|
|
6005
5907
|
return resultMap;
|
|
6006
5908
|
}
|
|
6007
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6008
5909
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6009
5910
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6010
5911
|
const results = await query;
|
|
@@ -6015,7 +5916,14 @@ var RelationService = class {
|
|
|
6015
5916
|
let parentId;
|
|
6016
5917
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6017
5918
|
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6018
|
-
if (parentId !== void 0 && parentIdSet.has(String(parentId)))
|
|
5919
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5920
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5921
|
+
resultMap.set(String(parentId), {
|
|
5922
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
5923
|
+
path: targetCollection.slug,
|
|
5924
|
+
values: parsedValues
|
|
5925
|
+
});
|
|
5926
|
+
}
|
|
6019
5927
|
}
|
|
6020
5928
|
return resultMap;
|
|
6021
5929
|
}
|
|
@@ -6029,9 +5937,9 @@ var RelationService = class {
|
|
|
6029
5937
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
6030
5938
|
const targetCollection = relation.target();
|
|
6031
5939
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6032
|
-
const targetPks =
|
|
5940
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6033
5941
|
const targetIdField = targetTable[targetPks[0].fieldName];
|
|
6034
|
-
const parentPks =
|
|
5942
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6035
5943
|
const parentIdInfo = parentPks[0];
|
|
6036
5944
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
6037
5945
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -6053,22 +5961,27 @@ var RelationService = class {
|
|
|
6053
5961
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
6054
5962
|
currentTable = joinTable;
|
|
6055
5963
|
}
|
|
6056
|
-
|
|
5964
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5965
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6057
5966
|
const results = await query;
|
|
6058
5967
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
6059
5968
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6060
5969
|
for (const row of results) {
|
|
6061
5970
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
6062
5971
|
const targetRow = row[targetTableName] || row;
|
|
6063
|
-
const parentId =
|
|
5972
|
+
const parentId = String(parentRow[parentIdInfo.fieldName]);
|
|
5973
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6064
5974
|
const arr = resultMap.get(parentId) || [];
|
|
6065
|
-
arr.push(
|
|
5975
|
+
arr.push({
|
|
5976
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
5977
|
+
path: targetCollection.slug,
|
|
5978
|
+
values: parsedValues
|
|
5979
|
+
});
|
|
6066
5980
|
resultMap.set(parentId, arr);
|
|
6067
5981
|
}
|
|
6068
5982
|
return resultMap;
|
|
6069
5983
|
}
|
|
6070
5984
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6071
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
6072
5985
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
6073
5986
|
if (!junctionTable) {
|
|
6074
5987
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -6087,13 +6000,17 @@ var RelationService = class {
|
|
|
6087
6000
|
const junctionData = row[relation.through.table] || row;
|
|
6088
6001
|
const targetData = row[targetTableName] || row;
|
|
6089
6002
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
6003
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
6090
6004
|
const arr = resultMap.get(parentId) || [];
|
|
6091
|
-
arr.push(
|
|
6005
|
+
arr.push({
|
|
6006
|
+
id: buildCompositeId(targetData, targetPks),
|
|
6007
|
+
path: targetCollection.slug,
|
|
6008
|
+
values: parsedValues
|
|
6009
|
+
});
|
|
6092
6010
|
resultMap.set(parentId, arr);
|
|
6093
6011
|
}
|
|
6094
6012
|
return resultMap;
|
|
6095
6013
|
}
|
|
6096
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6097
6014
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6098
6015
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6099
6016
|
const results = await query;
|
|
@@ -6106,9 +6023,14 @@ var RelationService = class {
|
|
|
6106
6023
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6107
6024
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6108
6025
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
6026
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6109
6027
|
const key = String(parentId);
|
|
6110
6028
|
const arr = resultMap.get(key) || [];
|
|
6111
|
-
arr.push(
|
|
6029
|
+
arr.push({
|
|
6030
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
6031
|
+
path: targetCollection.slug,
|
|
6032
|
+
values: parsedValues
|
|
6033
|
+
});
|
|
6112
6034
|
resultMap.set(key, arr);
|
|
6113
6035
|
}
|
|
6114
6036
|
}
|
|
@@ -6156,12 +6078,12 @@ var RelationService = class {
|
|
|
6156
6078
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
6157
6079
|
continue;
|
|
6158
6080
|
}
|
|
6159
|
-
const parentPks =
|
|
6081
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6160
6082
|
const parentIdInfo = parentPks[0];
|
|
6161
6083
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6162
6084
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6163
6085
|
if (targetEntityIds.length > 0) {
|
|
6164
|
-
const targetPks =
|
|
6086
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6165
6087
|
const targetIdInfo = targetPks[0];
|
|
6166
6088
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6167
6089
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6181,12 +6103,12 @@ var RelationService = class {
|
|
|
6181
6103
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6182
6104
|
continue;
|
|
6183
6105
|
}
|
|
6184
|
-
const parentPks =
|
|
6106
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6185
6107
|
const parentIdInfo = parentPks[0];
|
|
6186
6108
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6187
6109
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6188
6110
|
if (targetEntityIds.length > 0) {
|
|
6189
|
-
const targetPks =
|
|
6111
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6190
6112
|
const targetIdInfo = targetPks[0];
|
|
6191
6113
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6192
6114
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6197,7 +6119,7 @@ var RelationService = class {
|
|
|
6197
6119
|
} 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
6120
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6199
6121
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6200
|
-
const targetPks =
|
|
6122
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6201
6123
|
const targetIdInfo = targetPks[0];
|
|
6202
6124
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6203
6125
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6205,7 +6127,7 @@ var RelationService = class {
|
|
|
6205
6127
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6206
6128
|
continue;
|
|
6207
6129
|
}
|
|
6208
|
-
const parentPks =
|
|
6130
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6209
6131
|
const parentIdInfo = parentPks[0];
|
|
6210
6132
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6211
6133
|
if (targetEntityIds.length > 0) {
|
|
@@ -6225,9 +6147,9 @@ var RelationService = class {
|
|
|
6225
6147
|
try {
|
|
6226
6148
|
const targetCollection = relation.target();
|
|
6227
6149
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6228
|
-
const targetPks =
|
|
6150
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6229
6151
|
const targetIdInfo = targetPks[0];
|
|
6230
|
-
const sourcePks =
|
|
6152
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6231
6153
|
const sourceIdInfo = sourcePks[0];
|
|
6232
6154
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6233
6155
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6308,12 +6230,12 @@ var RelationService = class {
|
|
|
6308
6230
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6309
6231
|
return;
|
|
6310
6232
|
}
|
|
6311
|
-
const sourcePks =
|
|
6233
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6312
6234
|
const sourceIdInfo = sourcePks[0];
|
|
6313
6235
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6314
6236
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6315
6237
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6316
|
-
const targetPks =
|
|
6238
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6317
6239
|
const targetIdInfo = targetPks[0];
|
|
6318
6240
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6319
6241
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6321,7 +6243,7 @@ var RelationService = class {
|
|
|
6321
6243
|
}));
|
|
6322
6244
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6323
6245
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6324
|
-
const targetPks =
|
|
6246
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6325
6247
|
const targetIdInfo = targetPks[0];
|
|
6326
6248
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6327
6249
|
const newLink = {
|
|
@@ -6352,12 +6274,12 @@ var RelationService = class {
|
|
|
6352
6274
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6353
6275
|
return;
|
|
6354
6276
|
}
|
|
6355
|
-
const sourcePks =
|
|
6277
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6356
6278
|
const sourceIdInfo = sourcePks[0];
|
|
6357
6279
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6358
6280
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6359
6281
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6360
|
-
const targetPks =
|
|
6282
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6361
6283
|
const targetIdInfo = targetPks[0];
|
|
6362
6284
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6363
6285
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6378,12 +6300,12 @@ var RelationService = class {
|
|
|
6378
6300
|
const { relation, newTargetId } = upd;
|
|
6379
6301
|
const targetCollection = relation.target();
|
|
6380
6302
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6381
|
-
const targetPks =
|
|
6303
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6382
6304
|
const targetIdInfo = targetPks[0];
|
|
6383
6305
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6384
6306
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6385
6307
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6386
|
-
const parentPks =
|
|
6308
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6387
6309
|
const parentIdInfo = parentPks[0];
|
|
6388
6310
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6389
6311
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6454,7 +6376,7 @@ var RelationService = class {
|
|
|
6454
6376
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6455
6377
|
return;
|
|
6456
6378
|
}
|
|
6457
|
-
const targetPks =
|
|
6379
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6458
6380
|
const targetIdInfo = targetPks[0];
|
|
6459
6381
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6460
6382
|
const junctionData = {
|
|
@@ -6470,131 +6392,6 @@ var RelationService = class {
|
|
|
6470
6392
|
}
|
|
6471
6393
|
};
|
|
6472
6394
|
//#endregion
|
|
6473
|
-
//#region src/services/row-pipeline.ts
|
|
6474
|
-
/**
|
|
6475
|
-
* Whether a many-relation reaches its target through a junction table.
|
|
6476
|
-
*
|
|
6477
|
-
* Also used to build the drizzle `with` config, which is why it is exported:
|
|
6478
|
-
* the query has to nest one level deeper for a junction, and the row walk has
|
|
6479
|
-
* to unwrap that same level back out.
|
|
6480
|
-
*/
|
|
6481
|
-
function isJunctionRelation(relation) {
|
|
6482
|
-
if (relation.through) return true;
|
|
6483
|
-
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6484
|
-
return false;
|
|
6485
|
-
}
|
|
6486
|
-
/**
|
|
6487
|
-
* Reach the target row inside a junction row.
|
|
6488
|
-
*
|
|
6489
|
-
* A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
|
|
6490
|
-
* the foreign keys, and the target nested under one of them. The target is the
|
|
6491
|
-
* only object among them, so that is how it is found. A junction row that has
|
|
6492
|
-
* not been nested (no `with` on the join) has no object and is returned as-is.
|
|
6493
|
-
*/
|
|
6494
|
-
function unwrapJunctionRow(item) {
|
|
6495
|
-
const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
|
|
6496
|
-
return nestedKey ? item[nestedKey] : item;
|
|
6497
|
-
}
|
|
6498
|
-
/**
|
|
6499
|
-
* Give back the number a `number` property was declared to be.
|
|
6500
|
-
*
|
|
6501
|
-
* Postgres returns NUMERIC as a string and drizzle keeps it that way, since a
|
|
6502
|
-
* numeric can hold more precision than a double. But the collection declared
|
|
6503
|
-
* this property `number` and the OpenAPI spec this server publishes says
|
|
6504
|
-
* `type: number`, so serving `"9.99"` breaks its own contract — and breaks it
|
|
6505
|
-
* asymmetrically, because a create answers with the number and the read that
|
|
6506
|
-
* follows answers with the string. Any client that multiplies a price works
|
|
6507
|
-
* until the first refresh.
|
|
6508
|
-
*
|
|
6509
|
-
* Declaring a property `number` already accepts double precision — that is what
|
|
6510
|
-
* the admin has always parsed it to. Columns REST serves that no property
|
|
6511
|
-
* declares are left exactly as the database returned them.
|
|
6512
|
-
*/
|
|
6513
|
-
function coerceDeclaredNumber(value, property) {
|
|
6514
|
-
if (property?.type !== "number" || typeof value !== "string") return value;
|
|
6515
|
-
const parsed = parseFloat(value);
|
|
6516
|
-
return isNaN(parsed) ? null : parsed;
|
|
6517
|
-
}
|
|
6518
|
-
/** Apply {@link coerceDeclaredNumber} across a row, leaving every other column alone. */
|
|
6519
|
-
function coerceDeclaredNumbers(row, collection) {
|
|
6520
|
-
const properties = collection.properties;
|
|
6521
|
-
if (!properties) return row;
|
|
6522
|
-
const out = {};
|
|
6523
|
-
for (const [key, value] of Object.entries(row)) out[key] = coerceDeclaredNumber(value, properties[key]);
|
|
6524
|
-
return out;
|
|
6525
|
-
}
|
|
6526
|
-
/** Render one target row in the requested style. */
|
|
6527
|
-
function renderTarget(targetRow, targetCollection, style, registry) {
|
|
6528
|
-
if (style === "inline") return coerceDeclaredNumbers({ ...targetRow }, targetCollection);
|
|
6529
|
-
const address = relationTargetAddress(targetRow, targetCollection, registry);
|
|
6530
|
-
const path = targetCollection.slug;
|
|
6531
|
-
return createRelationRefWithData(address, path, {
|
|
6532
|
-
id: address,
|
|
6533
|
-
path,
|
|
6534
|
-
values: normalizeDbValues(targetRow, targetCollection)
|
|
6535
|
-
});
|
|
6536
|
-
}
|
|
6537
|
-
/**
|
|
6538
|
-
* The address a relation ref points at.
|
|
6539
|
-
*
|
|
6540
|
-
* The whole key, not its first column: a composite-keyed target addressed by
|
|
6541
|
-
* `tenant_id` alone points at every row that shares it. A target whose key
|
|
6542
|
-
* cannot be resolved at all used to throw here — reading `[0]` of an empty
|
|
6543
|
-
* array — taking down the parent's fetch over a relation it may not even have
|
|
6544
|
-
* asked for. The first column is a guess, but a ref that resolves to nothing
|
|
6545
|
-
* beats no rows at all.
|
|
6546
|
-
*/
|
|
6547
|
-
function relationTargetAddress(targetRow, targetCollection, registry) {
|
|
6548
|
-
const address = deriveRowAddress(targetRow, targetCollection, registry);
|
|
6549
|
-
if (address) return address;
|
|
6550
|
-
return String(targetRow[Object.keys(targetRow)[0]] ?? "");
|
|
6551
|
-
}
|
|
6552
|
-
/**
|
|
6553
|
-
* The row the admin renders: every column, with relations as references.
|
|
6554
|
-
*
|
|
6555
|
-
* Values are normalized (dates, numbers, NaN) because the admin's view-model
|
|
6556
|
-
* expects real types. The row's own address is *not* among the columns — it is
|
|
6557
|
-
* derived by the consumer from the collection's primary keys.
|
|
6558
|
-
*/
|
|
6559
|
-
function toCmsRow(row, collection, registry) {
|
|
6560
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6561
|
-
const normalized = normalizeDbValues(row, collection);
|
|
6562
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6563
|
-
const relData = row[relation.relationName || key];
|
|
6564
|
-
if (relData === void 0 || relData === null) continue;
|
|
6565
|
-
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6566
|
-
const targetCollection = relation.target();
|
|
6567
|
-
normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
|
|
6568
|
-
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
|
|
6569
|
-
}
|
|
6570
|
-
return normalized;
|
|
6571
|
-
}
|
|
6572
|
-
/**
|
|
6573
|
-
* The row REST serves: every column under its own name, with the value Postgres
|
|
6574
|
-
* returned, and relations inlined as the target's columns.
|
|
6575
|
-
*
|
|
6576
|
-
* Values are the ones the database returned, except where that contradicts the
|
|
6577
|
-
* declared type: a `number` property is served as a number (see
|
|
6578
|
-
* {@link coerceDeclaredNumber}). Dates stay as the database returned them —
|
|
6579
|
-
* JSON has its own opinions about dates that the admin's view-model does not
|
|
6580
|
-
* share.
|
|
6581
|
-
*
|
|
6582
|
-
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
6583
|
-
* the relations `include` asked for, so the row is the authority on which are
|
|
6584
|
-
* actually there.
|
|
6585
|
-
*/
|
|
6586
|
-
function toRestRow(row, collection, registry) {
|
|
6587
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6588
|
-
const flat = {};
|
|
6589
|
-
for (const [key, value] of Object.entries(row)) {
|
|
6590
|
-
const relation = findRelation(resolvedRelations, key);
|
|
6591
|
-
if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
|
|
6592
|
-
else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
|
|
6593
|
-
else flat[key] = coerceDeclaredNumber(value, collection.properties?.[key]);
|
|
6594
|
-
}
|
|
6595
|
-
return flat;
|
|
6596
|
-
}
|
|
6597
|
-
//#endregion
|
|
6598
6395
|
//#region src/services/FetchService.ts
|
|
6599
6396
|
/**
|
|
6600
6397
|
* Service for handling all row read operations.
|
|
@@ -6653,7 +6450,7 @@ var FetchService = class {
|
|
|
6653
6450
|
if (!shouldInclude(key)) continue;
|
|
6654
6451
|
const drizzleRelName = relation.relationName || key;
|
|
6655
6452
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6656
|
-
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
6453
|
+
if (relation.cardinality === "many" && this.isJunctionRelation(relation, collection)) {
|
|
6657
6454
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6658
6455
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6659
6456
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6662,6 +6459,14 @@ var FetchService = class {
|
|
|
6662
6459
|
return withConfig;
|
|
6663
6460
|
}
|
|
6664
6461
|
/**
|
|
6462
|
+
* Detect if a many-to-many relation uses a junction table in the Drizzle schema.
|
|
6463
|
+
*/
|
|
6464
|
+
isJunctionRelation(relation, _collection) {
|
|
6465
|
+
if (relation.through) return true;
|
|
6466
|
+
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6467
|
+
return false;
|
|
6468
|
+
}
|
|
6469
|
+
/**
|
|
6665
6470
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6666
6471
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6667
6472
|
*/
|
|
@@ -6670,6 +6475,68 @@ var FetchService = class {
|
|
|
6670
6475
|
return null;
|
|
6671
6476
|
}
|
|
6672
6477
|
/**
|
|
6478
|
+
* The address a relation ref points at.
|
|
6479
|
+
*
|
|
6480
|
+
* The whole key, not its first column: a composite-keyed target addressed
|
|
6481
|
+
* by `tenant_id` alone points at every row that shares it. And a target
|
|
6482
|
+
* whose key cannot be resolved at all used to throw here — reading
|
|
6483
|
+
* `targetPks[0]` of an empty array — taking down the parent's fetch over a
|
|
6484
|
+
* relation it may not even have asked for; the first column is a guess, but
|
|
6485
|
+
* a ref that resolves to nothing beats no rows at all.
|
|
6486
|
+
*/
|
|
6487
|
+
relationTargetAddress(targetRow, targetCollection) {
|
|
6488
|
+
const address = deriveRowAddress(targetRow, targetCollection, this.registry);
|
|
6489
|
+
if (address) return address;
|
|
6490
|
+
const firstColumn = targetRow[Object.keys(targetRow)[0]];
|
|
6491
|
+
return String(firstColumn ?? "");
|
|
6492
|
+
}
|
|
6493
|
+
/**
|
|
6494
|
+
* Convert a db.query result row (with nested relation objects) to a flat row.
|
|
6495
|
+
* Handles:
|
|
6496
|
+
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
6497
|
+
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
6498
|
+
* - Flattening junction-table many-to-many results
|
|
6499
|
+
*
|
|
6500
|
+
* The row's own address is not among them: it is derived by the consumer
|
|
6501
|
+
* from the collection's primary keys.
|
|
6502
|
+
*/
|
|
6503
|
+
drizzleResultToRow(row, collection) {
|
|
6504
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6505
|
+
const normalizedValues = normalizeDbValues(row, collection);
|
|
6506
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6507
|
+
const relData = row[relation.relationName || key];
|
|
6508
|
+
if (relData === void 0 || relData === null) continue;
|
|
6509
|
+
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6510
|
+
const targetCollection = relation.target();
|
|
6511
|
+
const targetPath = targetCollection.slug;
|
|
6512
|
+
normalizedValues[key] = relData.map((item) => {
|
|
6513
|
+
let targetRow = item;
|
|
6514
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6515
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6516
|
+
if (nestedKey) targetRow = item[nestedKey];
|
|
6517
|
+
}
|
|
6518
|
+
const relId = this.relationTargetAddress(targetRow, targetCollection);
|
|
6519
|
+
return createRelationRefWithData(relId, targetPath, {
|
|
6520
|
+
id: relId,
|
|
6521
|
+
path: targetPath,
|
|
6522
|
+
values: normalizeDbValues(targetRow, targetCollection)
|
|
6523
|
+
});
|
|
6524
|
+
});
|
|
6525
|
+
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
6526
|
+
const targetCollection = relation.target();
|
|
6527
|
+
const targetPath = targetCollection.slug;
|
|
6528
|
+
const relObj = relData;
|
|
6529
|
+
const relId = this.relationTargetAddress(relObj, targetCollection);
|
|
6530
|
+
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
6531
|
+
id: relId,
|
|
6532
|
+
path: targetPath,
|
|
6533
|
+
values: normalizeDbValues(relObj, targetCollection)
|
|
6534
|
+
});
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
return normalizedValues;
|
|
6538
|
+
}
|
|
6539
|
+
/**
|
|
6673
6540
|
* Post-fetch joinPath relations for a single flat row.
|
|
6674
6541
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6675
6542
|
* so they must be loaded separately after the primary query.
|
|
@@ -6700,10 +6567,8 @@ var FetchService = class {
|
|
|
6700
6567
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6701
6568
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6702
6569
|
if (joinPathRelations.length === 0) return;
|
|
6703
|
-
const
|
|
6704
|
-
|
|
6705
|
-
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6706
|
-
};
|
|
6570
|
+
const idInfo = idInfoArray[0];
|
|
6571
|
+
const parentIdOf = (row) => row[idInfo.fieldName];
|
|
6707
6572
|
for (const [key, relation] of joinPathRelations) try {
|
|
6708
6573
|
const addressable = rows.filter((r) => parentIdOf(r) !== void 0 && parentIdOf(r) !== null);
|
|
6709
6574
|
if (addressable.length === 0) continue;
|
|
@@ -6723,6 +6588,32 @@ var FetchService = class {
|
|
|
6723
6588
|
}
|
|
6724
6589
|
}
|
|
6725
6590
|
/**
|
|
6591
|
+
* Convert a db.query result row to a flat REST-style row with populated relations.
|
|
6592
|
+
*
|
|
6593
|
+
* Every column is copied through under its own name, with the value Postgres
|
|
6594
|
+
* returned. This used to open with a synthesized `id` and then skip the key
|
|
6595
|
+
* column, which renamed it (a `sku` primary key was served as `id`, and `sku`
|
|
6596
|
+
* did not appear at all) and restringified it (`42` → `"42"`). Consumers that
|
|
6597
|
+
* need an address derive it from the collection's primary keys.
|
|
6598
|
+
*/
|
|
6599
|
+
drizzleResultToRestRow(row, collection) {
|
|
6600
|
+
const flat = {};
|
|
6601
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6602
|
+
for (const [k, v] of Object.entries(row)) {
|
|
6603
|
+
const relation = findRelation(resolvedRelations, k);
|
|
6604
|
+
if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
|
|
6605
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6606
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6607
|
+
if (nestedKey) return { ...item[nestedKey] };
|
|
6608
|
+
}
|
|
6609
|
+
return { ...item };
|
|
6610
|
+
});
|
|
6611
|
+
else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) flat[k] = { ...v };
|
|
6612
|
+
else flat[k] = v;
|
|
6613
|
+
}
|
|
6614
|
+
return flat;
|
|
6615
|
+
}
|
|
6616
|
+
/**
|
|
6726
6617
|
* Build db.query-compatible options from standard fetch options.
|
|
6727
6618
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6728
6619
|
*/
|
|
@@ -6792,7 +6683,7 @@ var FetchService = class {
|
|
|
6792
6683
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6793
6684
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6794
6685
|
const table = getTableForCollection(collection, this.registry);
|
|
6795
|
-
const idInfoArray =
|
|
6686
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6796
6687
|
const idInfo = idInfoArray[0];
|
|
6797
6688
|
const idField = table[idInfo.fieldName];
|
|
6798
6689
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6806,7 +6697,7 @@ var FetchService = class {
|
|
|
6806
6697
|
with: withConfig
|
|
6807
6698
|
});
|
|
6808
6699
|
if (!row) return void 0;
|
|
6809
|
-
const flatRow =
|
|
6700
|
+
const flatRow = this.drizzleResultToRow(row, collection);
|
|
6810
6701
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6811
6702
|
return flatRow;
|
|
6812
6703
|
} catch (e) {
|
|
@@ -6848,7 +6739,7 @@ var FetchService = class {
|
|
|
6848
6739
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6849
6740
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6850
6741
|
const table = getTableForCollection(collection, this.registry);
|
|
6851
|
-
const idInfoArray =
|
|
6742
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6852
6743
|
const idInfo = idInfoArray[0];
|
|
6853
6744
|
const idField = table[idInfo.fieldName];
|
|
6854
6745
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6858,7 +6749,7 @@ var FetchService = class {
|
|
|
6858
6749
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6859
6750
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6860
6751
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6861
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6752
|
+
return (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRow(row, collection));
|
|
6862
6753
|
} catch (e) {
|
|
6863
6754
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6864
6755
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6919,10 +6810,8 @@ var FetchService = class {
|
|
|
6919
6810
|
}
|
|
6920
6811
|
/**
|
|
6921
6812
|
* Fallback path used when db.query is unavailable.
|
|
6922
|
-
*
|
|
6923
|
-
*
|
|
6924
|
-
* relations from what drizzle already nested — no query per row. This one
|
|
6925
|
-
* has no nesting to read, so it resolves relations itself, in batches.
|
|
6813
|
+
* The primary path uses drizzleResultToRow which handles relation
|
|
6814
|
+
* mapping without N+1 queries.
|
|
6926
6815
|
*
|
|
6927
6816
|
* Process raw database results into flat rows with relations.
|
|
6928
6817
|
*/
|
|
@@ -7064,7 +6953,7 @@ var FetchService = class {
|
|
|
7064
6953
|
if (value === void 0 || value === null) return true;
|
|
7065
6954
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7066
6955
|
const table = getTableForCollection(collection, this.registry);
|
|
7067
|
-
const idInfoArray =
|
|
6956
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7068
6957
|
const idInfo = idInfoArray[0];
|
|
7069
6958
|
const idField = table[idInfo.fieldName];
|
|
7070
6959
|
const field = table[fieldName];
|
|
@@ -7091,7 +6980,7 @@ var FetchService = class {
|
|
|
7091
6980
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
7092
6981
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7093
6982
|
const table = getTableForCollection(collection, this.registry);
|
|
7094
|
-
const idInfoArray =
|
|
6983
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7095
6984
|
const idInfo = idInfoArray[0];
|
|
7096
6985
|
const idField = table[idInfo.fieldName];
|
|
7097
6986
|
const tableName = getTableName(table);
|
|
@@ -7099,7 +6988,7 @@ var FetchService = class {
|
|
|
7099
6988
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
7100
6989
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
7101
6990
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
7102
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
6991
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRestRow(row, collection));
|
|
7103
6992
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
7104
6993
|
return restRows;
|
|
7105
6994
|
} catch (e) {
|
|
@@ -7148,7 +7037,7 @@ var FetchService = class {
|
|
|
7148
7037
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
7149
7038
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7150
7039
|
const table = getTableForCollection(collection, this.registry);
|
|
7151
|
-
const idInfoArray =
|
|
7040
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7152
7041
|
const idInfo = idInfoArray[0];
|
|
7153
7042
|
const idField = table[idInfo.fieldName];
|
|
7154
7043
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -7161,7 +7050,7 @@ var FetchService = class {
|
|
|
7161
7050
|
...withConfig ? { with: withConfig } : {}
|
|
7162
7051
|
});
|
|
7163
7052
|
if (!row) return null;
|
|
7164
|
-
const restRow =
|
|
7053
|
+
const restRow = this.drizzleResultToRestRow(row, collection);
|
|
7165
7054
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
7166
7055
|
return restRow;
|
|
7167
7056
|
} catch (e) {
|
|
@@ -7206,7 +7095,7 @@ var FetchService = class {
|
|
|
7206
7095
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7207
7096
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7208
7097
|
const table = getTableForCollection(collection, this.registry);
|
|
7209
|
-
const idField = table[
|
|
7098
|
+
const idField = table[getPrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7210
7099
|
let vectorMeta;
|
|
7211
7100
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7212
7101
|
let query = vectorMeta ? this.db.select({
|
|
@@ -7695,7 +7584,7 @@ var PersistService = class {
|
|
|
7695
7584
|
} catch (error) {
|
|
7696
7585
|
throw this.toUserFriendlyError(error, collection.slug);
|
|
7697
7586
|
}
|
|
7698
|
-
const finalEntity = await this.fetchService.
|
|
7587
|
+
const finalEntity = await this.fetchService.fetchOne(collection.slug, savedId, databaseId);
|
|
7699
7588
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
7700
7589
|
return finalEntity;
|
|
7701
7590
|
}
|
|
@@ -8614,14 +8503,12 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8614
8503
|
let updatedValues = values;
|
|
8615
8504
|
const contextForCallback = this.buildCallContext();
|
|
8616
8505
|
let previousValuesForHistory;
|
|
8617
|
-
if (status === "existing" && id)
|
|
8618
|
-
const existing = await this.dataService.
|
|
8506
|
+
if (status === "existing" && id) {
|
|
8507
|
+
const existing = await this.dataService.fetchOne(path, id, resolvedCollection?.databaseId);
|
|
8619
8508
|
if (existing) {
|
|
8620
8509
|
const { id: _existingId, ...existingValues } = existing;
|
|
8621
8510
|
previousValuesForHistory = existingValues;
|
|
8622
8511
|
}
|
|
8623
|
-
} catch (err) {
|
|
8624
|
-
logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
|
|
8625
8512
|
}
|
|
8626
8513
|
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
8627
8514
|
if (globalCallbacks?.beforeSave) {
|
|
@@ -9234,7 +9121,6 @@ var DatabasePoolManager = class {
|
|
|
9234
9121
|
pool.on("error", (err) => {
|
|
9235
9122
|
logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
|
|
9236
9123
|
});
|
|
9237
|
-
guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
|
|
9238
9124
|
this.pools.set(databaseName, pool);
|
|
9239
9125
|
return pool;
|
|
9240
9126
|
}
|
|
@@ -20795,22 +20681,14 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20795
20681
|
$$ LANGUAGE sql STABLE
|
|
20796
20682
|
`);
|
|
20797
20683
|
});
|
|
20798
|
-
|
|
20799
|
-
|
|
20800
|
-
|
|
20801
|
-
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
|
|
20805
|
-
|
|
20806
|
-
"is_anonymous BOOLEAN DEFAULT FALSE NOT NULL",
|
|
20807
|
-
"metadata JSONB DEFAULT '{}' NOT NULL",
|
|
20808
|
-
"created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL",
|
|
20809
|
-
"updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL"
|
|
20810
|
-
]) await db.execute(sql`
|
|
20811
|
-
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20812
|
-
ADD COLUMN IF NOT EXISTS ${sql.raw(columnDef)}
|
|
20813
|
-
`);
|
|
20684
|
+
await db.execute(sql`
|
|
20685
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20686
|
+
ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
|
|
20687
|
+
`);
|
|
20688
|
+
await db.execute(sql`
|
|
20689
|
+
ALTER TABLE ${sql.raw(usersTableName)}
|
|
20690
|
+
ADD COLUMN IF NOT EXISTS roles TEXT[] DEFAULT '{}' NOT NULL
|
|
20691
|
+
`);
|
|
20814
20692
|
try {
|
|
20815
20693
|
if ((await db.execute(sql`
|
|
20816
20694
|
SELECT EXISTS (
|
|
@@ -20881,34 +20759,6 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20881
20759
|
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
20882
20760
|
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
20883
20761
|
`);
|
|
20884
|
-
try {
|
|
20885
|
-
const authTablePairs = [
|
|
20886
|
-
[usersSchema, resolvedTable],
|
|
20887
|
-
[authSchema, "user_identities"],
|
|
20888
|
-
[authSchema, "refresh_tokens"],
|
|
20889
|
-
[authSchema, "password_reset_tokens"],
|
|
20890
|
-
[authSchema, "app_config"],
|
|
20891
|
-
[authSchema, "mfa_factors"],
|
|
20892
|
-
[authSchema, "mfa_challenges"],
|
|
20893
|
-
[authSchema, "recovery_codes"]
|
|
20894
|
-
];
|
|
20895
|
-
for (const [schemaName, tableName] of authTablePairs) if ((await db.execute(sql`
|
|
20896
|
-
SELECT 1
|
|
20897
|
-
FROM pg_class c
|
|
20898
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
20899
|
-
WHERE n.nspname = ${schemaName}
|
|
20900
|
-
AND c.relname = ${tableName}
|
|
20901
|
-
AND c.relforcerowsecurity
|
|
20902
|
-
`)).rows.length > 0) {
|
|
20903
|
-
await db.execute(sql`
|
|
20904
|
-
ALTER TABLE ${sql.raw(`"${schemaName}"."${tableName}"`)}
|
|
20905
|
-
NO FORCE ROW LEVEL SECURITY
|
|
20906
|
-
`);
|
|
20907
|
-
logger.warn(`🔧 Cleared stale FORCE ROW LEVEL SECURITY on "${schemaName}"."${tableName}" (legacy RLS model — it binds the owner connection and breaks privileged auth writes)`);
|
|
20908
|
-
}
|
|
20909
|
-
} catch (rlsReconcileError) {
|
|
20910
|
-
logger.warn(`⚠️ Could not reconcile FORCE ROW LEVEL SECURITY on auth tables: ${rlsReconcileError instanceof Error ? rlsReconcileError.message : String(rlsReconcileError)}`);
|
|
20911
|
-
}
|
|
20912
20762
|
logger.info("✅ Auth tables ready");
|
|
20913
20763
|
} catch (error) {
|
|
20914
20764
|
logger.error("❌ Failed to create auth tables", { error });
|
|
@@ -20956,31 +20806,6 @@ var UserService = class {
|
|
|
20956
20806
|
const name = getTableName(this.usersTable);
|
|
20957
20807
|
return `"${getTableConfig(this.usersTable).schema || "public"}"."${name}"`;
|
|
20958
20808
|
}
|
|
20959
|
-
/**
|
|
20960
|
-
* Run a privileged auth write with an explicitly cleared RLS context.
|
|
20961
|
-
*
|
|
20962
|
-
* The auth services run on the base/owner connection, which by design
|
|
20963
|
-
* carries a NULL `app.user_id` so the `auth.uid() IS NULL` server-escape
|
|
20964
|
-
* in the default policies applies. That NULL is normally guaranteed by
|
|
20965
|
-
* `set_config(..., is_local = true)` resetting at transaction end — but a
|
|
20966
|
-
* GUC that survives on a pooled connection (or a connection role that
|
|
20967
|
-
* doesn't bypass RLS: FORCE ROW LEVEL SECURITY, or a non-owner role)
|
|
20968
|
-
* turns the trusted write into an RLS-scoped one and denies it with
|
|
20969
|
-
* SQLSTATE 42501. Clearing the GUCs here, transaction-locally at the
|
|
20970
|
-
* single chokepoint, makes the server context deterministic instead of
|
|
20971
|
-
* trusting whatever state the pool hands us. `auth.uid()` reads '' as
|
|
20972
|
-
* NULL via NULLIF, so '' is the server context.
|
|
20973
|
-
*/
|
|
20974
|
-
async withServerContext(fn) {
|
|
20975
|
-
return await this.db.transaction(async (tx) => {
|
|
20976
|
-
await tx.execute(sql`
|
|
20977
|
-
SELECT set_config('app.user_id', '', true),
|
|
20978
|
-
set_config('app.user_roles', '', true),
|
|
20979
|
-
set_config('app.jwt', '', true)
|
|
20980
|
-
`);
|
|
20981
|
-
return await fn(tx);
|
|
20982
|
-
});
|
|
20983
|
-
}
|
|
20984
20809
|
mapRowToUser(row) {
|
|
20985
20810
|
if (!row) return row;
|
|
20986
20811
|
const id = row.id ?? row.uid;
|
|
@@ -21078,7 +20903,7 @@ var UserService = class {
|
|
|
21078
20903
|
}
|
|
21079
20904
|
async createUser(data) {
|
|
21080
20905
|
const payload = this.mapPayload(data);
|
|
21081
|
-
const [row] = await this.
|
|
20906
|
+
const [row] = await this.db.insert(this.usersTable).values(payload).returning();
|
|
21082
20907
|
return this.mapRowToUser(row);
|
|
21083
20908
|
}
|
|
21084
20909
|
async getUserById(id) {
|
|
@@ -21117,12 +20942,12 @@ var UserService = class {
|
|
|
21117
20942
|
}));
|
|
21118
20943
|
}
|
|
21119
20944
|
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
21120
|
-
await this.
|
|
20945
|
+
await this.db.insert(this.userIdentitiesTable).values({
|
|
21121
20946
|
userId,
|
|
21122
20947
|
provider,
|
|
21123
20948
|
providerId,
|
|
21124
20949
|
profileData: profileData || null
|
|
21125
|
-
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] })
|
|
20950
|
+
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
|
|
21126
20951
|
}
|
|
21127
20952
|
async updateUser(id, data) {
|
|
21128
20953
|
const idCol = getColumn(this.usersTable, "id");
|
|
@@ -21130,13 +20955,13 @@ var UserService = class {
|
|
|
21130
20955
|
const payload = this.mapPayload(data);
|
|
21131
20956
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21132
20957
|
payload[updatedAtKey] = /* @__PURE__ */ new Date();
|
|
21133
|
-
const [row] = await this.
|
|
20958
|
+
const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
|
|
21134
20959
|
return row ? this.mapRowToUser(row) : null;
|
|
21135
20960
|
}
|
|
21136
20961
|
async deleteUser(id) {
|
|
21137
20962
|
const idCol = getColumn(this.usersTable, "id");
|
|
21138
20963
|
if (!idCol) return;
|
|
21139
|
-
await this.
|
|
20964
|
+
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
21140
20965
|
}
|
|
21141
20966
|
async listUsers() {
|
|
21142
20967
|
return (await this.db.select().from(this.usersTable)).map((row) => this.mapRowToUser(row));
|
|
@@ -21190,10 +21015,10 @@ var UserService = class {
|
|
|
21190
21015
|
if (!idCol) return;
|
|
21191
21016
|
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
21192
21017
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21193
|
-
await this.
|
|
21018
|
+
await this.db.update(this.usersTable).set({
|
|
21194
21019
|
[passwordHashColKey]: passwordHash,
|
|
21195
21020
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21196
|
-
}).where(eq(idCol, id))
|
|
21021
|
+
}).where(eq(idCol, id));
|
|
21197
21022
|
}
|
|
21198
21023
|
/**
|
|
21199
21024
|
* Set email verification status
|
|
@@ -21204,11 +21029,11 @@ var UserService = class {
|
|
|
21204
21029
|
const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
21205
21030
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21206
21031
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21207
|
-
await this.
|
|
21032
|
+
await this.db.update(this.usersTable).set({
|
|
21208
21033
|
[emailVerifiedColKey]: verified,
|
|
21209
21034
|
[emailVerificationTokenColKey]: null,
|
|
21210
21035
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21211
|
-
}).where(eq(idCol, id))
|
|
21036
|
+
}).where(eq(idCol, id));
|
|
21212
21037
|
}
|
|
21213
21038
|
/**
|
|
21214
21039
|
* Set email verification token
|
|
@@ -21219,11 +21044,11 @@ var UserService = class {
|
|
|
21219
21044
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21220
21045
|
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
21221
21046
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21222
|
-
await this.
|
|
21047
|
+
await this.db.update(this.usersTable).set({
|
|
21223
21048
|
[emailVerificationTokenColKey]: token,
|
|
21224
21049
|
[emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
|
|
21225
21050
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21226
|
-
}).where(eq(idCol, id))
|
|
21051
|
+
}).where(eq(idCol, id));
|
|
21227
21052
|
}
|
|
21228
21053
|
/**
|
|
21229
21054
|
* Find user by email verification token
|
|
@@ -21268,22 +21093,22 @@ var UserService = class {
|
|
|
21268
21093
|
async setUserRoles(userId, roleIds) {
|
|
21269
21094
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21270
21095
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
21271
|
-
await this.
|
|
21096
|
+
await this.db.execute(sql`
|
|
21272
21097
|
UPDATE ${sql.raw(usersTableName)}
|
|
21273
21098
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
21274
21099
|
WHERE id = ${userId}
|
|
21275
|
-
`)
|
|
21100
|
+
`);
|
|
21276
21101
|
}
|
|
21277
21102
|
/**
|
|
21278
21103
|
* Assign a specific role to new user (appends if not present)
|
|
21279
21104
|
*/
|
|
21280
21105
|
async assignDefaultRole(userId, roleId) {
|
|
21281
21106
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21282
|
-
await this.
|
|
21107
|
+
await this.db.execute(sql`
|
|
21283
21108
|
UPDATE ${sql.raw(usersTableName)}
|
|
21284
21109
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
21285
21110
|
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
21286
|
-
`)
|
|
21111
|
+
`);
|
|
21287
21112
|
}
|
|
21288
21113
|
/**
|
|
21289
21114
|
* Get user with their roles
|
|
@@ -22753,7 +22578,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22753
22578
|
const wantsCdc = cdcMode !== "off";
|
|
22754
22579
|
const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
|
|
22755
22580
|
let cdcEnabled = false;
|
|
22756
|
-
let provisionCdcForTables;
|
|
22757
22581
|
if (wantsCdc && !directUrl) {
|
|
22758
22582
|
const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
|
|
22759
22583
|
if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
|
|
@@ -22770,9 +22594,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22770
22594
|
})).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
|
|
22771
22595
|
await realtimeService.enableCdc(directUrl);
|
|
22772
22596
|
cdcEnabled = true;
|
|
22773
|
-
provisionCdcForTables = async (tables) => {
|
|
22774
|
-
await provisionTriggerCdc(cdcRunSql, tables);
|
|
22775
|
-
};
|
|
22776
22597
|
logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.`);
|
|
22777
22598
|
} catch (err) {
|
|
22778
22599
|
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
|
|
@@ -22797,14 +22618,13 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22797
22618
|
const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
|
|
22798
22619
|
const missing = [];
|
|
22799
22620
|
for (const col of registeredCollections) {
|
|
22800
|
-
if (col.auth?.enabled) continue;
|
|
22801
22621
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
22802
22622
|
const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
|
|
22803
22623
|
const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
|
|
22804
22624
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
22805
22625
|
if (!dbTables.has(fullCheckName)) missing.push({
|
|
22806
22626
|
slug: col.slug,
|
|
22807
|
-
table:
|
|
22627
|
+
table: checkName
|
|
22808
22628
|
});
|
|
22809
22629
|
}
|
|
22810
22630
|
if (missing.length > 0) {
|
|
@@ -22838,8 +22658,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22838
22658
|
registry,
|
|
22839
22659
|
realtimeService,
|
|
22840
22660
|
driver,
|
|
22841
|
-
poolManager
|
|
22842
|
-
provisionCdcForTables
|
|
22661
|
+
poolManager
|
|
22843
22662
|
}
|
|
22844
22663
|
};
|
|
22845
22664
|
},
|
|
@@ -22851,18 +22670,6 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22851
22670
|
const registry = internals.registry;
|
|
22852
22671
|
const authCollection = authConfig.collection;
|
|
22853
22672
|
await ensureAuthTablesExist(db, authCollection);
|
|
22854
|
-
if (authCollection && internals.provisionCdcForTables) {
|
|
22855
|
-
const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
|
|
22856
|
-
const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
|
|
22857
|
-
if (authTable) try {
|
|
22858
|
-
await internals.provisionCdcForTables([{
|
|
22859
|
-
schema: authSchema,
|
|
22860
|
-
table: authTable
|
|
22861
|
-
}]);
|
|
22862
|
-
} catch (err) {
|
|
22863
|
-
logger.warn(`⚠️ [CDC] Could not attach change-capture to the auth table "${authSchema}.${authTable}" — writes to it won't emit database-level events.`, { detail: err instanceof Error ? err.message : String(err) });
|
|
22864
|
-
}
|
|
22865
|
-
}
|
|
22866
22673
|
let emailService;
|
|
22867
22674
|
if (authConfig.email) emailService = createEmailService(authConfig.email);
|
|
22868
22675
|
const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
|
|
@@ -22933,6 +22740,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
22933
22740
|
};
|
|
22934
22741
|
}
|
|
22935
22742
|
//#endregion
|
|
22936
|
-
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor,
|
|
22743
|
+
export { AuthenticatedPostgresBackendDriver, BackupToolError, BranchService, DatabasePoolManager, DrizzleConditionBuilder, PostgresBackendDriver, PostgresCollectionRegistry, PostgresConditionBuilder, PostgresRealtimeProvider, RealtimeService, appConfig, backupCronConfigFromEnv, buildBackupFilename, buildPgDumpArgs, buildPgRestoreArgs, checkToolServerCompatibility, createAuthSchema, createBackupCron, createDirectDatabaseConnection, createDump, createPostgresAdapter, createPostgresBootstrapper, createPostgresDatabaseConnection, createPostgresWebSocket, createReadReplicaConnection, detectToolMajor, ensureDatabaseExists, generateSchema, getServerVersionMajor, joinStorageKey, listBackups, magicLinkTokens, magicLinkTokensRelations, mfaChallenges, mfaChallengesRelations, mfaFactors, mfaFactorsRelations, parseBackupDestination, parseBackupTimestamp, parseDbNameFromUrl, parsePgToolMajor, passwordResetTokens, passwordResetTokensRelations, preflight, pruneBackups, recoveryCodes, recoveryCodesRelations, refreshTokens, refreshTokensRelations, resolveConnectionString, resolvePgBinary, restoreDump, selectBackupsToPrune, serverVersionNumToMajor, uploadBackup, userIdentities, userIdentitiesRelations, users, usersRelations, usersSchema, withDatabaseName };
|
|
22937
22744
|
|
|
22938
22745
|
//# sourceMappingURL=index.es.js.map
|