@rebasepro/server-postgres 0.9.1-canary.58368ce → 0.9.1-canary.682a9cf
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 +10 -0
- package/dist/auth/services.d.ts +16 -0
- package/dist/connection.d.ts +21 -0
- package/dist/index.es.js +455 -243
- package/dist/index.es.js.map +1 -1
- package/dist/schema/doctor.d.ts +1 -1
- package/dist/services/FetchService.d.ts +4 -38
- package/dist/services/RelationService.d.ts +34 -0
- package/dist/services/collection-helpers.d.ts +34 -7
- package/dist/services/row-pipeline.d.ts +63 -0
- package/package.json +10 -7
- package/src/PostgresBackendDriver.ts +18 -5
- package/src/PostgresBootstrapper.ts +51 -2
- package/src/auth/ensure-tables.ts +73 -11
- package/src/auth/services.ts +49 -19
- package/src/connection.ts +61 -1
- package/src/databasePoolManager.ts +2 -0
- package/src/schema/doctor.ts +45 -20
- package/src/schema/introspect-db.ts +19 -2
- package/src/services/FetchService.ts +29 -163
- package/src/services/PersistService.ts +9 -2
- package/src/services/RelationService.ts +153 -93
- package/src/services/collection-helpers.ts +59 -27
- package/src/services/realtimeService.ts +4 -2
- package/src/services/row-pipeline.ts +239 -0
- package/src/utils/drizzle-conditions.ts +13 -0
package/dist/index.es.js
CHANGED
|
@@ -71,16 +71,51 @@ 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
|
|
74
|
+
createReadReplicaConnection: () => createReadReplicaConnection,
|
|
75
|
+
guardPoolAgainstDirtyRelease: () => guardPoolAgainstDirtyRelease
|
|
75
76
|
});
|
|
76
77
|
var DEFAULT_POOL = {
|
|
77
78
|
max: 20,
|
|
78
79
|
idleTimeoutMillis: 3e4,
|
|
79
80
|
connectionTimeoutMillis: 1e4,
|
|
80
|
-
queryTimeout:
|
|
81
|
+
queryTimeout: 6e4,
|
|
81
82
|
statementTimeout: 3e4,
|
|
82
83
|
keepAlive: true
|
|
83
84
|
};
|
|
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
|
+
}
|
|
84
119
|
/**
|
|
85
120
|
* Create a Drizzle-backed Postgres connection with a production-grade
|
|
86
121
|
* connection pool.
|
|
@@ -112,6 +147,7 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
|
|
|
112
147
|
logger.error("[pg-pool] Unexpected pool error", { detail: err.message });
|
|
113
148
|
if (err.message.includes("ETIMEDOUT")) logger.warn("[pg-pool] Connection timeout detected — pool will auto-retry");
|
|
114
149
|
});
|
|
150
|
+
guardPoolAgainstDirtyRelease(pool, "pg-pool");
|
|
115
151
|
return {
|
|
116
152
|
db: schema ? drizzle(pool, { schema }) : drizzle(pool),
|
|
117
153
|
pool,
|
|
@@ -144,6 +180,7 @@ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
|
|
|
144
180
|
pool.on("error", (err) => {
|
|
145
181
|
logger.error("[pg-direct-pool] Unexpected pool error", { detail: err.message });
|
|
146
182
|
});
|
|
183
|
+
guardPoolAgainstDirtyRelease(pool, "pg-direct-pool");
|
|
147
184
|
return {
|
|
148
185
|
db: schema ? drizzle(pool, { schema }) : drizzle(pool),
|
|
149
186
|
pool,
|
|
@@ -173,6 +210,7 @@ function createReadReplicaConnection(connectionString, schema, poolConfig) {
|
|
|
173
210
|
pool.on("error", (err) => {
|
|
174
211
|
logger.error("[pg-replica-pool] Unexpected pool error", { detail: err.message });
|
|
175
212
|
});
|
|
213
|
+
guardPoolAgainstDirtyRelease(pool, "pg-replica-pool");
|
|
176
214
|
return {
|
|
177
215
|
db: schema ? drizzle(pool, { schema }) : drizzle(pool),
|
|
178
216
|
pool,
|
|
@@ -4549,8 +4587,26 @@ function getTableForCollection(collection, registry) {
|
|
|
4549
4587
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4550
4588
|
return table;
|
|
4551
4589
|
}
|
|
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
|
+
*/
|
|
4552
4609
|
function getPrimaryKeys(collection, registry) {
|
|
4553
|
-
const table = getTableForCollection(collection, registry);
|
|
4554
4610
|
if (collection.properties) {
|
|
4555
4611
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4556
4612
|
fieldName: key,
|
|
@@ -4559,6 +4615,8 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4559
4615
|
}));
|
|
4560
4616
|
if (idProps.length > 0) return idProps;
|
|
4561
4617
|
}
|
|
4618
|
+
const table = registry.getTable(getTableName$1(collection));
|
|
4619
|
+
if (!table) return [];
|
|
4562
4620
|
const keys = [];
|
|
4563
4621
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4564
4622
|
const col = colRaw;
|
|
@@ -4587,6 +4645,21 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4587
4645
|
return keys;
|
|
4588
4646
|
}
|
|
4589
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
|
+
/**
|
|
4590
4663
|
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
4591
4664
|
* instead.
|
|
4592
4665
|
*
|
|
@@ -4613,12 +4686,7 @@ function findUnresolvableKeyCollections(collections, registry) {
|
|
|
4613
4686
|
const findings = [];
|
|
4614
4687
|
for (const collection of collections) {
|
|
4615
4688
|
if (getDeclaredPrimaryKeys(collection).length > 0) continue;
|
|
4616
|
-
|
|
4617
|
-
try {
|
|
4618
|
-
keys = getPrimaryKeys(collection, registry);
|
|
4619
|
-
} catch {
|
|
4620
|
-
continue;
|
|
4621
|
-
}
|
|
4689
|
+
const keys = getPrimaryKeys(collection, registry);
|
|
4622
4690
|
if (keys.length === 0) continue;
|
|
4623
4691
|
if (keys.length === 1 && keys[0].fieldName === "id") continue;
|
|
4624
4692
|
findings.push({
|
|
@@ -4649,19 +4717,14 @@ function warnOnKeysTheAdminCannotResolve(collections, registry) {
|
|
|
4649
4717
|
* The address of a row: derived from the collection's primary keys, because a
|
|
4650
4718
|
* row does not carry one — it is exactly its columns.
|
|
4651
4719
|
*
|
|
4652
|
-
* Falls back to a literal `id` column,
|
|
4653
|
-
*
|
|
4654
|
-
*
|
|
4655
|
-
* (
|
|
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).
|
|
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).
|
|
4659
4724
|
*/
|
|
4660
4725
|
function deriveRowAddress(row, collection, registry) {
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4664
|
-
} catch {}
|
|
4726
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
|
|
4727
|
+
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4665
4728
|
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
4666
4729
|
return "";
|
|
4667
4730
|
}
|
|
@@ -5203,6 +5266,7 @@ var DrizzleConditionBuilder = class {
|
|
|
5203
5266
|
static buildVectorSearchConditions(table, vectorSearch) {
|
|
5204
5267
|
const column = table[vectorSearch.property];
|
|
5205
5268
|
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");
|
|
5206
5270
|
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
5207
5271
|
const distanceFn = vectorSearch.distance || "cosine";
|
|
5208
5272
|
let operator;
|
|
@@ -5703,6 +5767,63 @@ var RelationService = class {
|
|
|
5703
5767
|
this.registry = registry;
|
|
5704
5768
|
}
|
|
5705
5769
|
/**
|
|
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
|
+
/**
|
|
5706
5827
|
* Fetch rows related to a parent row through a specific relation
|
|
5707
5828
|
*/
|
|
5708
5829
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5721,9 +5842,9 @@ var RelationService = class {
|
|
|
5721
5842
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5722
5843
|
const targetCollection = relation.target();
|
|
5723
5844
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5724
|
-
const idInfo =
|
|
5845
|
+
const idInfo = requirePrimaryKeys(targetCollection, this.registry);
|
|
5725
5846
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5726
|
-
const parentPks =
|
|
5847
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5727
5848
|
const parentIdInfo = parentPks[0];
|
|
5728
5849
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5729
5850
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5747,7 +5868,7 @@ var RelationService = class {
|
|
|
5747
5868
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5748
5869
|
currentTable = joinTable;
|
|
5749
5870
|
}
|
|
5750
|
-
const parentIdField = parentTable[
|
|
5871
|
+
const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5751
5872
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5752
5873
|
if (options.limit) query = query.limit(options.limit);
|
|
5753
5874
|
const results = await query;
|
|
@@ -5755,12 +5876,7 @@ var RelationService = class {
|
|
|
5755
5876
|
const rows = [];
|
|
5756
5877
|
for (const row of results) {
|
|
5757
5878
|
const targetRow = row[targetTableName] || row;
|
|
5758
|
-
|
|
5759
|
-
rows.push({
|
|
5760
|
-
id: buildCompositeId(targetRow, idInfo),
|
|
5761
|
-
path: targetCollection.slug,
|
|
5762
|
-
values: parsedValues
|
|
5763
|
-
});
|
|
5879
|
+
rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
|
|
5764
5880
|
}
|
|
5765
5881
|
return rows;
|
|
5766
5882
|
}
|
|
@@ -5779,12 +5895,7 @@ var RelationService = class {
|
|
|
5779
5895
|
const rows = [];
|
|
5780
5896
|
for (const row of results) {
|
|
5781
5897
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5782
|
-
|
|
5783
|
-
rows.push({
|
|
5784
|
-
id: buildCompositeId(targetRow, idInfo),
|
|
5785
|
-
path: targetCollection.slug,
|
|
5786
|
-
values: parsedValues
|
|
5787
|
-
});
|
|
5898
|
+
rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
|
|
5788
5899
|
}
|
|
5789
5900
|
return rows;
|
|
5790
5901
|
}
|
|
@@ -5801,8 +5912,8 @@ var RelationService = class {
|
|
|
5801
5912
|
}
|
|
5802
5913
|
const targetCollection = relation.target();
|
|
5803
5914
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5804
|
-
const targetIdField = targetTable[
|
|
5805
|
-
const parentPks =
|
|
5915
|
+
const targetIdField = targetTable[requirePrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5916
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5806
5917
|
const parentIdInfo = parentPks[0];
|
|
5807
5918
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5808
5919
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5821,10 +5932,10 @@ var RelationService = class {
|
|
|
5821
5932
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5822
5933
|
const targetCollection = relation.target();
|
|
5823
5934
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5824
|
-
const targetPks =
|
|
5935
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
5825
5936
|
const targetIdInfo = targetPks[0];
|
|
5826
5937
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5827
|
-
const parentPks =
|
|
5938
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5828
5939
|
const parentIdInfo = parentPks[0];
|
|
5829
5940
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5830
5941
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5848,25 +5959,19 @@ var RelationService = class {
|
|
|
5848
5959
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5849
5960
|
currentTable = joinTable;
|
|
5850
5961
|
}
|
|
5851
|
-
|
|
5852
|
-
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5962
|
+
query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
5853
5963
|
const results = await query;
|
|
5854
5964
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5855
5965
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5856
5966
|
for (const row of results) {
|
|
5857
5967
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5858
5968
|
const targetRow = row[targetTableName] || row;
|
|
5859
|
-
|
|
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
|
-
});
|
|
5969
|
+
resultMap.set(buildCompositeId(parentRow, parentPks), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5866
5970
|
}
|
|
5867
5971
|
return resultMap;
|
|
5868
5972
|
}
|
|
5869
5973
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5974
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5870
5975
|
const localKeyCol = parentTable[relation.localKey];
|
|
5871
5976
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5872
5977
|
const fkRows = await this.db.select({
|
|
@@ -5895,17 +6000,11 @@ var RelationService = class {
|
|
|
5895
6000
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5896
6001
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
5897
6002
|
const targetRow = targetById.get(String(fkValue));
|
|
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
|
-
}
|
|
6003
|
+
if (targetRow) resultMap.set(parentIdStr, await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5906
6004
|
}
|
|
5907
6005
|
return resultMap;
|
|
5908
6006
|
}
|
|
6007
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
5909
6008
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
5910
6009
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5911
6010
|
const results = await query;
|
|
@@ -5916,14 +6015,7 @@ var RelationService = class {
|
|
|
5916
6015
|
let parentId;
|
|
5917
6016
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
5918
6017
|
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
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
|
-
}
|
|
6018
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5927
6019
|
}
|
|
5928
6020
|
return resultMap;
|
|
5929
6021
|
}
|
|
@@ -5937,9 +6029,9 @@ var RelationService = class {
|
|
|
5937
6029
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5938
6030
|
const targetCollection = relation.target();
|
|
5939
6031
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5940
|
-
const targetPks =
|
|
6032
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
5941
6033
|
const targetIdField = targetTable[targetPks[0].fieldName];
|
|
5942
|
-
const parentPks =
|
|
6034
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5943
6035
|
const parentIdInfo = parentPks[0];
|
|
5944
6036
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5945
6037
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5961,27 +6053,22 @@ var RelationService = class {
|
|
|
5961
6053
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5962
6054
|
currentTable = joinTable;
|
|
5963
6055
|
}
|
|
5964
|
-
|
|
5965
|
-
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6056
|
+
query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
5966
6057
|
const results = await query;
|
|
5967
6058
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5968
6059
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5969
6060
|
for (const row of results) {
|
|
5970
6061
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5971
6062
|
const targetRow = row[targetTableName] || row;
|
|
5972
|
-
const parentId =
|
|
5973
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6063
|
+
const parentId = buildCompositeId(parentRow, parentPks);
|
|
5974
6064
|
const arr = resultMap.get(parentId) || [];
|
|
5975
|
-
arr.push(
|
|
5976
|
-
id: buildCompositeId(targetRow, targetPks),
|
|
5977
|
-
path: targetCollection.slug,
|
|
5978
|
-
values: parsedValues
|
|
5979
|
-
});
|
|
6065
|
+
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5980
6066
|
resultMap.set(parentId, arr);
|
|
5981
6067
|
}
|
|
5982
6068
|
return resultMap;
|
|
5983
6069
|
}
|
|
5984
6070
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6071
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
5985
6072
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
5986
6073
|
if (!junctionTable) {
|
|
5987
6074
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -6000,17 +6087,13 @@ var RelationService = class {
|
|
|
6000
6087
|
const junctionData = row[relation.through.table] || row;
|
|
6001
6088
|
const targetData = row[targetTableName] || row;
|
|
6002
6089
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
6003
|
-
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
6004
6090
|
const arr = resultMap.get(parentId) || [];
|
|
6005
|
-
arr.push(
|
|
6006
|
-
id: buildCompositeId(targetData, targetPks),
|
|
6007
|
-
path: targetCollection.slug,
|
|
6008
|
-
values: parsedValues
|
|
6009
|
-
});
|
|
6091
|
+
arr.push(await this.toRelatedRow(targetData, targetCollection, targetPks));
|
|
6010
6092
|
resultMap.set(parentId, arr);
|
|
6011
6093
|
}
|
|
6012
6094
|
return resultMap;
|
|
6013
6095
|
}
|
|
6096
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6014
6097
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6015
6098
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6016
6099
|
const results = await query;
|
|
@@ -6023,14 +6106,9 @@ var RelationService = class {
|
|
|
6023
6106
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6024
6107
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6025
6108
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
6026
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6027
6109
|
const key = String(parentId);
|
|
6028
6110
|
const arr = resultMap.get(key) || [];
|
|
6029
|
-
arr.push(
|
|
6030
|
-
id: buildCompositeId(targetRow, targetPks),
|
|
6031
|
-
path: targetCollection.slug,
|
|
6032
|
-
values: parsedValues
|
|
6033
|
-
});
|
|
6111
|
+
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
6034
6112
|
resultMap.set(key, arr);
|
|
6035
6113
|
}
|
|
6036
6114
|
}
|
|
@@ -6078,12 +6156,12 @@ var RelationService = class {
|
|
|
6078
6156
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
6079
6157
|
continue;
|
|
6080
6158
|
}
|
|
6081
|
-
const parentPks =
|
|
6159
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6082
6160
|
const parentIdInfo = parentPks[0];
|
|
6083
6161
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6084
6162
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6085
6163
|
if (targetEntityIds.length > 0) {
|
|
6086
|
-
const targetPks =
|
|
6164
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6087
6165
|
const targetIdInfo = targetPks[0];
|
|
6088
6166
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6089
6167
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6103,12 +6181,12 @@ var RelationService = class {
|
|
|
6103
6181
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6104
6182
|
continue;
|
|
6105
6183
|
}
|
|
6106
|
-
const parentPks =
|
|
6184
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6107
6185
|
const parentIdInfo = parentPks[0];
|
|
6108
6186
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6109
6187
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6110
6188
|
if (targetEntityIds.length > 0) {
|
|
6111
|
-
const targetPks =
|
|
6189
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6112
6190
|
const targetIdInfo = targetPks[0];
|
|
6113
6191
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6114
6192
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6119,7 +6197,7 @@ var RelationService = class {
|
|
|
6119
6197
|
} 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.`);
|
|
6120
6198
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6121
6199
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6122
|
-
const targetPks =
|
|
6200
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6123
6201
|
const targetIdInfo = targetPks[0];
|
|
6124
6202
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6125
6203
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6127,7 +6205,7 @@ var RelationService = class {
|
|
|
6127
6205
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6128
6206
|
continue;
|
|
6129
6207
|
}
|
|
6130
|
-
const parentPks =
|
|
6208
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6131
6209
|
const parentIdInfo = parentPks[0];
|
|
6132
6210
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6133
6211
|
if (targetEntityIds.length > 0) {
|
|
@@ -6147,9 +6225,9 @@ var RelationService = class {
|
|
|
6147
6225
|
try {
|
|
6148
6226
|
const targetCollection = relation.target();
|
|
6149
6227
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6150
|
-
const targetPks =
|
|
6228
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6151
6229
|
const targetIdInfo = targetPks[0];
|
|
6152
|
-
const sourcePks =
|
|
6230
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6153
6231
|
const sourceIdInfo = sourcePks[0];
|
|
6154
6232
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6155
6233
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6230,12 +6308,12 @@ var RelationService = class {
|
|
|
6230
6308
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6231
6309
|
return;
|
|
6232
6310
|
}
|
|
6233
|
-
const sourcePks =
|
|
6311
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6234
6312
|
const sourceIdInfo = sourcePks[0];
|
|
6235
6313
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6236
6314
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6237
6315
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6238
|
-
const targetPks =
|
|
6316
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6239
6317
|
const targetIdInfo = targetPks[0];
|
|
6240
6318
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6241
6319
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6243,7 +6321,7 @@ var RelationService = class {
|
|
|
6243
6321
|
}));
|
|
6244
6322
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6245
6323
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6246
|
-
const targetPks =
|
|
6324
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6247
6325
|
const targetIdInfo = targetPks[0];
|
|
6248
6326
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6249
6327
|
const newLink = {
|
|
@@ -6274,12 +6352,12 @@ var RelationService = class {
|
|
|
6274
6352
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6275
6353
|
return;
|
|
6276
6354
|
}
|
|
6277
|
-
const sourcePks =
|
|
6355
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6278
6356
|
const sourceIdInfo = sourcePks[0];
|
|
6279
6357
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6280
6358
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6281
6359
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6282
|
-
const targetPks =
|
|
6360
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6283
6361
|
const targetIdInfo = targetPks[0];
|
|
6284
6362
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6285
6363
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6300,12 +6378,12 @@ var RelationService = class {
|
|
|
6300
6378
|
const { relation, newTargetId } = upd;
|
|
6301
6379
|
const targetCollection = relation.target();
|
|
6302
6380
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6303
|
-
const targetPks =
|
|
6381
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6304
6382
|
const targetIdInfo = targetPks[0];
|
|
6305
6383
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6306
6384
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6307
6385
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6308
|
-
const parentPks =
|
|
6386
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
6309
6387
|
const parentIdInfo = parentPks[0];
|
|
6310
6388
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6311
6389
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6376,7 +6454,7 @@ var RelationService = class {
|
|
|
6376
6454
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6377
6455
|
return;
|
|
6378
6456
|
}
|
|
6379
|
-
const targetPks =
|
|
6457
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6380
6458
|
const targetIdInfo = targetPks[0];
|
|
6381
6459
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6382
6460
|
const junctionData = {
|
|
@@ -6392,6 +6470,150 @@ var RelationService = class {
|
|
|
6392
6470
|
}
|
|
6393
6471
|
};
|
|
6394
6472
|
//#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
|
|
6395
6617
|
//#region src/services/FetchService.ts
|
|
6396
6618
|
/**
|
|
6397
6619
|
* Service for handling all row read operations.
|
|
@@ -6450,7 +6672,7 @@ var FetchService = class {
|
|
|
6450
6672
|
if (!shouldInclude(key)) continue;
|
|
6451
6673
|
const drizzleRelName = relation.relationName || key;
|
|
6452
6674
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6453
|
-
if (relation.cardinality === "many" &&
|
|
6675
|
+
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
6454
6676
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6455
6677
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6456
6678
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6459,14 +6681,6 @@ var FetchService = class {
|
|
|
6459
6681
|
return withConfig;
|
|
6460
6682
|
}
|
|
6461
6683
|
/**
|
|
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
|
-
/**
|
|
6470
6684
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6471
6685
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6472
6686
|
*/
|
|
@@ -6475,68 +6689,6 @@ var FetchService = class {
|
|
|
6475
6689
|
return null;
|
|
6476
6690
|
}
|
|
6477
6691
|
/**
|
|
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
|
-
/**
|
|
6540
6692
|
* Post-fetch joinPath relations for a single flat row.
|
|
6541
6693
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6542
6694
|
* so they must be loaded separately after the primary query.
|
|
@@ -6567,8 +6719,10 @@ var FetchService = class {
|
|
|
6567
6719
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6568
6720
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6569
6721
|
if (joinPathRelations.length === 0) return;
|
|
6570
|
-
const
|
|
6571
|
-
|
|
6722
|
+
const parentIdOf = (row) => {
|
|
6723
|
+
const address = buildCompositeId(row, idInfoArray);
|
|
6724
|
+
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6725
|
+
};
|
|
6572
6726
|
for (const [key, relation] of joinPathRelations) try {
|
|
6573
6727
|
const addressable = rows.filter((r) => parentIdOf(r) !== void 0 && parentIdOf(r) !== null);
|
|
6574
6728
|
if (addressable.length === 0) continue;
|
|
@@ -6588,32 +6742,6 @@ var FetchService = class {
|
|
|
6588
6742
|
}
|
|
6589
6743
|
}
|
|
6590
6744
|
/**
|
|
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
|
-
/**
|
|
6617
6745
|
* Build db.query-compatible options from standard fetch options.
|
|
6618
6746
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6619
6747
|
*/
|
|
@@ -6683,7 +6811,7 @@ var FetchService = class {
|
|
|
6683
6811
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6684
6812
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6685
6813
|
const table = getTableForCollection(collection, this.registry);
|
|
6686
|
-
const idInfoArray =
|
|
6814
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6687
6815
|
const idInfo = idInfoArray[0];
|
|
6688
6816
|
const idField = table[idInfo.fieldName];
|
|
6689
6817
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6697,7 +6825,7 @@ var FetchService = class {
|
|
|
6697
6825
|
with: withConfig
|
|
6698
6826
|
});
|
|
6699
6827
|
if (!row) return void 0;
|
|
6700
|
-
const flatRow =
|
|
6828
|
+
const flatRow = toCmsRow(row, collection, this.registry);
|
|
6701
6829
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6702
6830
|
return flatRow;
|
|
6703
6831
|
} catch (e) {
|
|
@@ -6739,7 +6867,7 @@ var FetchService = class {
|
|
|
6739
6867
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6740
6868
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6741
6869
|
const table = getTableForCollection(collection, this.registry);
|
|
6742
|
-
const idInfoArray =
|
|
6870
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6743
6871
|
const idInfo = idInfoArray[0];
|
|
6744
6872
|
const idField = table[idInfo.fieldName];
|
|
6745
6873
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6749,7 +6877,7 @@ var FetchService = class {
|
|
|
6749
6877
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6750
6878
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6751
6879
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6752
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6880
|
+
return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
|
|
6753
6881
|
} catch (e) {
|
|
6754
6882
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6755
6883
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6810,8 +6938,10 @@ var FetchService = class {
|
|
|
6810
6938
|
}
|
|
6811
6939
|
/**
|
|
6812
6940
|
* Fallback path used when db.query is unavailable.
|
|
6813
|
-
*
|
|
6814
|
-
*
|
|
6941
|
+
*
|
|
6942
|
+
* The primary path runs the results through `toCmsRow`, which maps
|
|
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.
|
|
6815
6945
|
*
|
|
6816
6946
|
* Process raw database results into flat rows with relations.
|
|
6817
6947
|
*/
|
|
@@ -6953,7 +7083,7 @@ var FetchService = class {
|
|
|
6953
7083
|
if (value === void 0 || value === null) return true;
|
|
6954
7084
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6955
7085
|
const table = getTableForCollection(collection, this.registry);
|
|
6956
|
-
const idInfoArray =
|
|
7086
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6957
7087
|
const idInfo = idInfoArray[0];
|
|
6958
7088
|
const idField = table[idInfo.fieldName];
|
|
6959
7089
|
const field = table[fieldName];
|
|
@@ -6980,7 +7110,7 @@ var FetchService = class {
|
|
|
6980
7110
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
6981
7111
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6982
7112
|
const table = getTableForCollection(collection, this.registry);
|
|
6983
|
-
const idInfoArray =
|
|
7113
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6984
7114
|
const idInfo = idInfoArray[0];
|
|
6985
7115
|
const idField = table[idInfo.fieldName];
|
|
6986
7116
|
const tableName = getTableName(table);
|
|
@@ -6988,7 +7118,7 @@ var FetchService = class {
|
|
|
6988
7118
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
6989
7119
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
6990
7120
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
6991
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
7121
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => toRestRow(row, collection, this.registry));
|
|
6992
7122
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
6993
7123
|
return restRows;
|
|
6994
7124
|
} catch (e) {
|
|
@@ -7037,7 +7167,7 @@ var FetchService = class {
|
|
|
7037
7167
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
7038
7168
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7039
7169
|
const table = getTableForCollection(collection, this.registry);
|
|
7040
|
-
const idInfoArray =
|
|
7170
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
7041
7171
|
const idInfo = idInfoArray[0];
|
|
7042
7172
|
const idField = table[idInfo.fieldName];
|
|
7043
7173
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -7050,7 +7180,7 @@ var FetchService = class {
|
|
|
7050
7180
|
...withConfig ? { with: withConfig } : {}
|
|
7051
7181
|
});
|
|
7052
7182
|
if (!row) return null;
|
|
7053
|
-
const restRow =
|
|
7183
|
+
const restRow = toRestRow(row, collection, this.registry);
|
|
7054
7184
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
7055
7185
|
return restRow;
|
|
7056
7186
|
} catch (e) {
|
|
@@ -7095,7 +7225,7 @@ var FetchService = class {
|
|
|
7095
7225
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7096
7226
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7097
7227
|
const table = getTableForCollection(collection, this.registry);
|
|
7098
|
-
const idField = table[
|
|
7228
|
+
const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7099
7229
|
let vectorMeta;
|
|
7100
7230
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7101
7231
|
let query = vectorMeta ? this.db.select({
|
|
@@ -7584,7 +7714,7 @@ var PersistService = class {
|
|
|
7584
7714
|
} catch (error) {
|
|
7585
7715
|
throw this.toUserFriendlyError(error, collection.slug);
|
|
7586
7716
|
}
|
|
7587
|
-
const finalEntity = await this.fetchService.
|
|
7717
|
+
const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, void 0, databaseId);
|
|
7588
7718
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
7589
7719
|
return finalEntity;
|
|
7590
7720
|
}
|
|
@@ -8503,12 +8633,14 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8503
8633
|
let updatedValues = values;
|
|
8504
8634
|
const contextForCallback = this.buildCallContext();
|
|
8505
8635
|
let previousValuesForHistory;
|
|
8506
|
-
if (status === "existing" && id) {
|
|
8507
|
-
const existing = await this.dataService.
|
|
8636
|
+
if (status === "existing" && id) try {
|
|
8637
|
+
const existing = await this.dataService.getFetchService().fetchOneForRest(path, id, void 0, resolvedCollection?.databaseId);
|
|
8508
8638
|
if (existing) {
|
|
8509
8639
|
const { id: _existingId, ...existingValues } = existing;
|
|
8510
8640
|
previousValuesForHistory = existingValues;
|
|
8511
8641
|
}
|
|
8642
|
+
} catch (err) {
|
|
8643
|
+
logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
|
|
8512
8644
|
}
|
|
8513
8645
|
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
8514
8646
|
if (globalCallbacks?.beforeSave) {
|
|
@@ -9121,6 +9253,7 @@ var DatabasePoolManager = class {
|
|
|
9121
9253
|
pool.on("error", (err) => {
|
|
9122
9254
|
logger.error(`[DatabasePoolManager] Unexpected error on idle client for db ${databaseName}`, { error: err });
|
|
9123
9255
|
});
|
|
9256
|
+
guardPoolAgainstDirtyRelease(pool, `pg-pool:${databaseName}`);
|
|
9124
9257
|
this.pools.set(databaseName, pool);
|
|
9125
9258
|
return pool;
|
|
9126
9259
|
}
|
|
@@ -20681,14 +20814,22 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20681
20814
|
$$ LANGUAGE sql STABLE
|
|
20682
20815
|
`);
|
|
20683
20816
|
});
|
|
20684
|
-
|
|
20685
|
-
|
|
20686
|
-
|
|
20687
|
-
|
|
20688
|
-
|
|
20689
|
-
|
|
20690
|
-
|
|
20691
|
-
|
|
20817
|
+
for (const columnDef of [
|
|
20818
|
+
"display_name VARCHAR(255)",
|
|
20819
|
+
"photo_url VARCHAR(500)",
|
|
20820
|
+
"roles TEXT[] DEFAULT '{}' NOT NULL",
|
|
20821
|
+
"password_hash VARCHAR(255)",
|
|
20822
|
+
"email_verified BOOLEAN DEFAULT FALSE NOT NULL",
|
|
20823
|
+
"email_verification_token VARCHAR(255)",
|
|
20824
|
+
"email_verification_sent_at TIMESTAMP WITH TIME ZONE",
|
|
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
|
+
`);
|
|
20692
20833
|
try {
|
|
20693
20834
|
if ((await db.execute(sql`
|
|
20694
20835
|
SELECT EXISTS (
|
|
@@ -20759,6 +20900,34 @@ async function ensureAuthTablesExist(db, collection) {
|
|
|
20759
20900
|
CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
|
|
20760
20901
|
ON ${sql.raw(recoveryCodesTableName)}(user_id)
|
|
20761
20902
|
`);
|
|
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
|
+
}
|
|
20762
20931
|
logger.info("✅ Auth tables ready");
|
|
20763
20932
|
} catch (error) {
|
|
20764
20933
|
logger.error("❌ Failed to create auth tables", { error });
|
|
@@ -20806,6 +20975,31 @@ var UserService = class {
|
|
|
20806
20975
|
const name = getTableName(this.usersTable);
|
|
20807
20976
|
return `"${getTableConfig(this.usersTable).schema || "public"}"."${name}"`;
|
|
20808
20977
|
}
|
|
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
|
+
}
|
|
20809
21003
|
mapRowToUser(row) {
|
|
20810
21004
|
if (!row) return row;
|
|
20811
21005
|
const id = row.id ?? row.uid;
|
|
@@ -20903,7 +21097,7 @@ var UserService = class {
|
|
|
20903
21097
|
}
|
|
20904
21098
|
async createUser(data) {
|
|
20905
21099
|
const payload = this.mapPayload(data);
|
|
20906
|
-
const [row] = await this.db.insert(this.usersTable).values(payload).returning();
|
|
21100
|
+
const [row] = await this.withServerContext(async (db) => await db.insert(this.usersTable).values(payload).returning());
|
|
20907
21101
|
return this.mapRowToUser(row);
|
|
20908
21102
|
}
|
|
20909
21103
|
async getUserById(id) {
|
|
@@ -20942,12 +21136,12 @@ var UserService = class {
|
|
|
20942
21136
|
}));
|
|
20943
21137
|
}
|
|
20944
21138
|
async linkUserIdentity(userId, provider, providerId, profileData) {
|
|
20945
|
-
await this.db.insert(this.userIdentitiesTable).values({
|
|
21139
|
+
await this.withServerContext(async (db) => db.insert(this.userIdentitiesTable).values({
|
|
20946
21140
|
userId,
|
|
20947
21141
|
provider,
|
|
20948
21142
|
providerId,
|
|
20949
21143
|
profileData: profileData || null
|
|
20950
|
-
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] });
|
|
21144
|
+
}).onConflictDoNothing({ target: [this.userIdentitiesTable.provider, this.userIdentitiesTable.providerId] }));
|
|
20951
21145
|
}
|
|
20952
21146
|
async updateUser(id, data) {
|
|
20953
21147
|
const idCol = getColumn(this.usersTable, "id");
|
|
@@ -20955,13 +21149,13 @@ var UserService = class {
|
|
|
20955
21149
|
const payload = this.mapPayload(data);
|
|
20956
21150
|
const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
20957
21151
|
payload[updatedAtKey] = /* @__PURE__ */ new Date();
|
|
20958
|
-
const [row] = await this.db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning();
|
|
21152
|
+
const [row] = await this.withServerContext(async (db) => await db.update(this.usersTable).set(payload).where(eq(idCol, id)).returning());
|
|
20959
21153
|
return row ? this.mapRowToUser(row) : null;
|
|
20960
21154
|
}
|
|
20961
21155
|
async deleteUser(id) {
|
|
20962
21156
|
const idCol = getColumn(this.usersTable, "id");
|
|
20963
21157
|
if (!idCol) return;
|
|
20964
|
-
await this.db.delete(this.usersTable).where(eq(idCol, id));
|
|
21158
|
+
await this.withServerContext(async (db) => db.delete(this.usersTable).where(eq(idCol, id)));
|
|
20965
21159
|
}
|
|
20966
21160
|
async listUsers() {
|
|
20967
21161
|
return (await this.db.select().from(this.usersTable)).map((row) => this.mapRowToUser(row));
|
|
@@ -21015,10 +21209,10 @@ var UserService = class {
|
|
|
21015
21209
|
if (!idCol) return;
|
|
21016
21210
|
const passwordHashColKey = getColumnKey(this.usersTable, "passwordHash", "password_hash") || "passwordHash";
|
|
21017
21211
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21018
|
-
await this.db.update(this.usersTable).set({
|
|
21212
|
+
await this.withServerContext(async (db) => db.update(this.usersTable).set({
|
|
21019
21213
|
[passwordHashColKey]: passwordHash,
|
|
21020
21214
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21021
|
-
}).where(eq(idCol, id));
|
|
21215
|
+
}).where(eq(idCol, id)));
|
|
21022
21216
|
}
|
|
21023
21217
|
/**
|
|
21024
21218
|
* Set email verification status
|
|
@@ -21029,11 +21223,11 @@ var UserService = class {
|
|
|
21029
21223
|
const emailVerifiedColKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
|
|
21030
21224
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21031
21225
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21032
|
-
await this.db.update(this.usersTable).set({
|
|
21226
|
+
await this.withServerContext(async (db) => db.update(this.usersTable).set({
|
|
21033
21227
|
[emailVerifiedColKey]: verified,
|
|
21034
21228
|
[emailVerificationTokenColKey]: null,
|
|
21035
21229
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21036
|
-
}).where(eq(idCol, id));
|
|
21230
|
+
}).where(eq(idCol, id)));
|
|
21037
21231
|
}
|
|
21038
21232
|
/**
|
|
21039
21233
|
* Set email verification token
|
|
@@ -21044,11 +21238,11 @@ var UserService = class {
|
|
|
21044
21238
|
const emailVerificationTokenColKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
|
|
21045
21239
|
const emailVerificationSentAtColKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
|
|
21046
21240
|
const updatedAtColKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
|
|
21047
|
-
await this.db.update(this.usersTable).set({
|
|
21241
|
+
await this.withServerContext(async (db) => db.update(this.usersTable).set({
|
|
21048
21242
|
[emailVerificationTokenColKey]: token,
|
|
21049
21243
|
[emailVerificationSentAtColKey]: token ? /* @__PURE__ */ new Date() : null,
|
|
21050
21244
|
[updatedAtColKey]: /* @__PURE__ */ new Date()
|
|
21051
|
-
}).where(eq(idCol, id));
|
|
21245
|
+
}).where(eq(idCol, id)));
|
|
21052
21246
|
}
|
|
21053
21247
|
/**
|
|
21054
21248
|
* Find user by email verification token
|
|
@@ -21093,22 +21287,22 @@ var UserService = class {
|
|
|
21093
21287
|
async setUserRoles(userId, roleIds) {
|
|
21094
21288
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21095
21289
|
const rolesArray = `{${roleIds.join(",")}}`;
|
|
21096
|
-
await this.db.execute(sql`
|
|
21290
|
+
await this.withServerContext(async (db) => db.execute(sql`
|
|
21097
21291
|
UPDATE ${sql.raw(usersTableName)}
|
|
21098
21292
|
SET roles = ${rolesArray}::text[], updated_at = NOW()
|
|
21099
21293
|
WHERE id = ${userId}
|
|
21100
|
-
`);
|
|
21294
|
+
`));
|
|
21101
21295
|
}
|
|
21102
21296
|
/**
|
|
21103
21297
|
* Assign a specific role to new user (appends if not present)
|
|
21104
21298
|
*/
|
|
21105
21299
|
async assignDefaultRole(userId, roleId) {
|
|
21106
21300
|
const usersTableName = this.getQualifiedUsersTableName();
|
|
21107
|
-
await this.db.execute(sql`
|
|
21301
|
+
await this.withServerContext(async (db) => db.execute(sql`
|
|
21108
21302
|
UPDATE ${sql.raw(usersTableName)}
|
|
21109
21303
|
SET roles = array_append(roles, ${roleId}), updated_at = NOW()
|
|
21110
21304
|
WHERE id = ${userId} AND NOT (${roleId} = ANY(roles))
|
|
21111
|
-
`);
|
|
21305
|
+
`));
|
|
21112
21306
|
}
|
|
21113
21307
|
/**
|
|
21114
21308
|
* Get user with their roles
|
|
@@ -22578,6 +22772,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22578
22772
|
const wantsCdc = cdcMode !== "off";
|
|
22579
22773
|
const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
|
|
22580
22774
|
let cdcEnabled = false;
|
|
22775
|
+
let provisionCdcForTables;
|
|
22581
22776
|
if (wantsCdc && !directUrl) {
|
|
22582
22777
|
const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
|
|
22583
22778
|
if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
|
|
@@ -22594,6 +22789,9 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22594
22789
|
})).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
|
|
22595
22790
|
await realtimeService.enableCdc(directUrl);
|
|
22596
22791
|
cdcEnabled = true;
|
|
22792
|
+
provisionCdcForTables = async (tables) => {
|
|
22793
|
+
await provisionTriggerCdc(cdcRunSql, tables);
|
|
22794
|
+
};
|
|
22597
22795
|
logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.`);
|
|
22598
22796
|
} catch (err) {
|
|
22599
22797
|
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
|
|
@@ -22618,13 +22816,14 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22618
22816
|
const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
|
|
22619
22817
|
const missing = [];
|
|
22620
22818
|
for (const col of registeredCollections) {
|
|
22819
|
+
if (col.auth?.enabled) continue;
|
|
22621
22820
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
22622
22821
|
const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
|
|
22623
22822
|
const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
|
|
22624
22823
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
22625
22824
|
if (!dbTables.has(fullCheckName)) missing.push({
|
|
22626
22825
|
slug: col.slug,
|
|
22627
|
-
table:
|
|
22826
|
+
table: fullCheckName
|
|
22628
22827
|
});
|
|
22629
22828
|
}
|
|
22630
22829
|
if (missing.length > 0) {
|
|
@@ -22658,7 +22857,8 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22658
22857
|
registry,
|
|
22659
22858
|
realtimeService,
|
|
22660
22859
|
driver,
|
|
22661
|
-
poolManager
|
|
22860
|
+
poolManager,
|
|
22861
|
+
provisionCdcForTables
|
|
22662
22862
|
}
|
|
22663
22863
|
};
|
|
22664
22864
|
},
|
|
@@ -22670,6 +22870,18 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22670
22870
|
const registry = internals.registry;
|
|
22671
22871
|
const authCollection = authConfig.collection;
|
|
22672
22872
|
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
|
+
}
|
|
22673
22885
|
let emailService;
|
|
22674
22886
|
if (authConfig.email) emailService = createEmailService(authConfig.email);
|
|
22675
22887
|
const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
|
|
@@ -22740,6 +22952,6 @@ function createPostgresAdapter(pgConfig) {
|
|
|
22740
22952
|
};
|
|
22741
22953
|
}
|
|
22742
22954
|
//#endregion
|
|
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 };
|
|
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, guardPoolAgainstDirtyRelease, 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 };
|
|
22744
22956
|
|
|
22745
22957
|
//# sourceMappingURL=index.es.js.map
|