@rebasepro/server-postgres 0.9.1-canary.d198c11 → 0.9.1-canary.d906fb7
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/collections/buildRegistry.d.ts +27 -0
- package/dist/data-transformer.d.ts +9 -2
- package/dist/index.es.js +474 -292
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +4 -32
- package/dist/services/RelationService.d.ts +34 -1
- package/dist/services/collection-helpers.d.ts +76 -0
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +7 -0
- package/dist/services/row-pipeline.d.ts +63 -0
- package/package.json +10 -7
- package/src/PostgresBackendDriver.ts +39 -11
- package/src/PostgresBootstrapper.ts +62 -25
- package/src/collections/buildRegistry.ts +59 -0
- package/src/data-transformer.ts +11 -9
- package/src/schema/introspect-db.ts +19 -2
- package/src/services/FetchService.ts +50 -229
- package/src/services/PersistService.ts +9 -2
- package/src/services/RelationService.ts +153 -94
- package/src/services/collection-helpers.ts +166 -3
- package/src/services/index.ts +1 -0
- package/src/services/realtimeService.ts +40 -19
- package/src/services/row-pipeline.ts +215 -0
- package/src/utils/drizzle-conditions.ts +13 -0
package/dist/index.es.js
CHANGED
|
@@ -1808,12 +1808,17 @@ function parseIdValues(idValue, primaryKeys) {
|
|
|
1808
1808
|
/**
|
|
1809
1809
|
* The primary keys of a collection, as declared by its properties.
|
|
1810
1810
|
*
|
|
1811
|
-
*
|
|
1812
|
-
*
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1811
|
+
* This is the only tier both sides can read, because it is the only one written
|
|
1812
|
+
* in the config: the postgres driver can also infer keys from the Drizzle
|
|
1813
|
+
* schema, which the browser never sees and is never sent — the admin compiles
|
|
1814
|
+
* the collection files into its own bundle rather than being served them. A key
|
|
1815
|
+
* that lives only in the Drizzle schema is therefore invisible here, and the
|
|
1816
|
+
* server says so at boot (`warnOnKeysTheAdminCannotResolve`) naming the `isId`
|
|
1817
|
+
* to add.
|
|
1818
|
+
*
|
|
1819
|
+
* Returns an empty array when a collection declares none, which callers must
|
|
1820
|
+
* treat as "not addressable" rather than defaulting to `id`: guessing a key
|
|
1821
|
+
* that is not the real one produces confidently wrong addresses.
|
|
1817
1822
|
*/
|
|
1818
1823
|
function getDeclaredPrimaryKeys(collection) {
|
|
1819
1824
|
const properties = collection.properties;
|
|
@@ -1838,9 +1843,15 @@ function getDeclaredPrimaryKeys(collection) {
|
|
|
1838
1843
|
* The postgres driver tries, in order: properties marked `isId`; the primary
|
|
1839
1844
|
* keys of the Drizzle schema; and finally a column literally named `id`. Only
|
|
1840
1845
|
* the first and last are visible in a `CollectionConfig`, which is what both
|
|
1841
|
-
* sides share
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1846
|
+
* sides share.
|
|
1847
|
+
*
|
|
1848
|
+
* So the two agree except on a collection that declares no `isId` and whose key
|
|
1849
|
+
* is known only to Drizzle. There, the driver reads the real key, and this
|
|
1850
|
+
* either resolves nothing (reported to the console by the caller) or — if the
|
|
1851
|
+
* table happens to have an unrelated `id` property — resolves `id`, which is
|
|
1852
|
+
* the wrong key and cannot be detected from here: the addresses look right and
|
|
1853
|
+
* route wrong. Only the config can settle it, so the server names both cases
|
|
1854
|
+
* at boot (`warnOnKeysTheAdminCannotResolve`) with the `isId` to add.
|
|
1844
1855
|
*/
|
|
1845
1856
|
function resolvePrimaryKeys(collection) {
|
|
1846
1857
|
const declared = getDeclaredPrimaryKeys(collection);
|
|
@@ -4095,7 +4106,7 @@ function createPrimaryKeyResolver(options) {
|
|
|
4095
4106
|
}
|
|
4096
4107
|
if (!warned.has(slug)) {
|
|
4097
4108
|
warned.add(slug);
|
|
4098
|
-
console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config.`);
|
|
4109
|
+
console.warn(`[rebase] Collection '${slug}' declares no primary key, so its rows have no address: detail links, caching and relations will not work for it. Mark the key property with \`isId\` in its collection config — the server logs which column to mark at boot, if its schema knows the key.`);
|
|
4099
4110
|
}
|
|
4100
4111
|
return keys;
|
|
4101
4112
|
};
|
|
@@ -4538,8 +4549,26 @@ function getTableForCollection(collection, registry) {
|
|
|
4538
4549
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4539
4550
|
return table;
|
|
4540
4551
|
}
|
|
4552
|
+
/**
|
|
4553
|
+
* The key columns a collection's rows are addressed by.
|
|
4554
|
+
*
|
|
4555
|
+
* Three tiers, in order: properties marked `isId`, the primary keys of the
|
|
4556
|
+
* drizzle schema, and finally a column literally named `id`. Only the first is
|
|
4557
|
+
* visible to the browser, which is why a key known only to drizzle is reported
|
|
4558
|
+
* at boot — see {@link warnOnKeysTheAdminCannotResolve}.
|
|
4559
|
+
*
|
|
4560
|
+
* Returns `[]` when nothing resolves, rather than throwing. It used to open by
|
|
4561
|
+
* resolving the table, which throws when there is none — so the `isId` tier,
|
|
4562
|
+
* which needs no table at all, was unreachable for exactly the collections
|
|
4563
|
+
* most likely to have no table registered. Every caller that wanted "no keys"
|
|
4564
|
+
* to mean "no keys" had to spell that out in a try/catch.
|
|
4565
|
+
*
|
|
4566
|
+
* Callers that cannot proceed without a key must say so themselves, naming the
|
|
4567
|
+
* collection: an empty array here means "this collection has no address", which
|
|
4568
|
+
* is a different answer in a notification (broadcast a wildcard) than in a save
|
|
4569
|
+
* (fail).
|
|
4570
|
+
*/
|
|
4541
4571
|
function getPrimaryKeys(collection, registry) {
|
|
4542
|
-
const table = getTableForCollection(collection, registry);
|
|
4543
4572
|
if (collection.properties) {
|
|
4544
4573
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4545
4574
|
fieldName: key,
|
|
@@ -4548,6 +4577,8 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4548
4577
|
}));
|
|
4549
4578
|
if (idProps.length > 0) return idProps;
|
|
4550
4579
|
}
|
|
4580
|
+
const table = registry.getTable(getTableName$1(collection));
|
|
4581
|
+
if (!table) return [];
|
|
4551
4582
|
const keys = [];
|
|
4552
4583
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4553
4584
|
const col = colRaw;
|
|
@@ -4575,6 +4606,90 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4575
4606
|
}
|
|
4576
4607
|
return keys;
|
|
4577
4608
|
}
|
|
4609
|
+
/**
|
|
4610
|
+
* The key columns, for callers that cannot do their job without one.
|
|
4611
|
+
*
|
|
4612
|
+
* {@link getPrimaryKeys} answers "what keys, if any" and returns `[]` for a
|
|
4613
|
+
* collection with no address. Most of this driver, though, is building a WHERE
|
|
4614
|
+
* clause and has no meaning without a key — for those, an empty array is not an
|
|
4615
|
+
* answer, and indexing `[0]` into it produces `Cannot read properties of
|
|
4616
|
+
* undefined` three frames from where the real problem is. This says what is
|
|
4617
|
+
* wrong and which collection it is wrong about.
|
|
4618
|
+
*/
|
|
4619
|
+
function requirePrimaryKeys(collection, registry) {
|
|
4620
|
+
const keys = getPrimaryKeys(collection, registry);
|
|
4621
|
+
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.`);
|
|
4622
|
+
return keys;
|
|
4623
|
+
}
|
|
4624
|
+
/**
|
|
4625
|
+
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
4626
|
+
* instead.
|
|
4627
|
+
*
|
|
4628
|
+
* The two sides resolve keys from different evidence. This driver reads, in
|
|
4629
|
+
* order: properties marked `isId`, the primary keys of the Drizzle schema, then
|
|
4630
|
+
* a column literally named `id`. The admin shares the `CollectionConfig` — it
|
|
4631
|
+
* compiles the same collection files into its bundle — but never the Drizzle
|
|
4632
|
+
* schema, so the middle tier is invisible to it.
|
|
4633
|
+
*
|
|
4634
|
+
* Nothing can normalize this at runtime: the server does not serve the admin
|
|
4635
|
+
* its collections, so a key resolved here cannot be handed over there. The
|
|
4636
|
+
* config files are the only thing both sides read, so the fix is an edit to
|
|
4637
|
+
* them, and the most this can do is say exactly which edit.
|
|
4638
|
+
*
|
|
4639
|
+
* Two shapes, and the second is the dangerous one:
|
|
4640
|
+
*
|
|
4641
|
+
* - No `isId`, no `id` property → the admin resolves no address, warns in the
|
|
4642
|
+
* console, and rows cannot be opened or linked.
|
|
4643
|
+
* - No `isId`, but an `id` property that is *not* the key → the admin addresses
|
|
4644
|
+
* rows by `id` while this driver reads the address as the real key. Nothing
|
|
4645
|
+
* errors: the addresses look right and route wrong.
|
|
4646
|
+
*/
|
|
4647
|
+
function findUnresolvableKeyCollections(collections, registry) {
|
|
4648
|
+
const findings = [];
|
|
4649
|
+
for (const collection of collections) {
|
|
4650
|
+
if (getDeclaredPrimaryKeys(collection).length > 0) continue;
|
|
4651
|
+
const keys = getPrimaryKeys(collection, registry);
|
|
4652
|
+
if (keys.length === 0) continue;
|
|
4653
|
+
if (keys.length === 1 && keys[0].fieldName === "id") continue;
|
|
4654
|
+
findings.push({
|
|
4655
|
+
collection,
|
|
4656
|
+
keys,
|
|
4657
|
+
shadowedByIdProperty: Boolean(collection.properties?.id)
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
return findings;
|
|
4661
|
+
}
|
|
4662
|
+
/**
|
|
4663
|
+
* Report the collections from {@link findUnresolvableKeyCollections} at boot,
|
|
4664
|
+
* with the edit that fixes each one.
|
|
4665
|
+
*
|
|
4666
|
+
* Grouped by failure, not by collection: the shadowed case is a routing bug and
|
|
4667
|
+
* the silent case is a missing feature, and they deserve different urgency.
|
|
4668
|
+
*/
|
|
4669
|
+
function warnOnKeysTheAdminCannotResolve(collections, registry) {
|
|
4670
|
+
const findings = findUnresolvableKeyCollections(collections, registry);
|
|
4671
|
+
if (findings.length === 0) return;
|
|
4672
|
+
const edit = (f) => `${f.collection.slug}: mark ${f.keys.map((k) => `\`${k.fieldName}\``).join(" and ")} with \`isId: ${f.keys[0].isUUID ? "\"uuid\"" : f.keys[0].type === "number" ? "\"increment\"" : "true"}\``;
|
|
4673
|
+
const shadowed = findings.filter((f) => f.shadowedByIdProperty);
|
|
4674
|
+
const silent = findings.filter((f) => !f.shadowedByIdProperty);
|
|
4675
|
+
if (shadowed.length > 0) logger.warn("⚠️ These collections declare no `isId`, and their key is only in the drizzle schema — but they do have a property called `id`. The admin has no way to know `id` is not the key, so it will address rows by it while this server reads the address as the real key: the links look right and route wrong. Nothing will error.\n\n" + shadowed.map((f) => ` • ${edit(f)}`).join("\n") + "\n");
|
|
4676
|
+
if (silent.length > 0) logger.warn("⚠️ These collections declare no `isId`, and their key is only in the drizzle schema, which the admin never sees. It will resolve no address for their rows, so detail links, caching and relations will not work for them:\n\n" + silent.map((f) => ` • ${edit(f)}`).join("\n") + "\n");
|
|
4677
|
+
}
|
|
4678
|
+
/**
|
|
4679
|
+
* The address of a row: derived from the collection's primary keys, because a
|
|
4680
|
+
* row does not carry one — it is exactly its columns.
|
|
4681
|
+
*
|
|
4682
|
+
* Falls back to a literal `id` column, for a row that reached us from somewhere
|
|
4683
|
+
* other than this driver. Returns `""` when there is no key and no `id` —
|
|
4684
|
+
* callers decide what that means, since "unaddressable" is a different answer
|
|
4685
|
+
* in a notification (broadcast a wildcard) than in a save (fail).
|
|
4686
|
+
*/
|
|
4687
|
+
function deriveRowAddress(row, collection, registry) {
|
|
4688
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
|
|
4689
|
+
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4690
|
+
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
4691
|
+
return "";
|
|
4692
|
+
}
|
|
4578
4693
|
//#endregion
|
|
4579
4694
|
//#region src/utils/drizzle-conditions.ts
|
|
4580
4695
|
/**
|
|
@@ -5113,6 +5228,7 @@ var DrizzleConditionBuilder = class {
|
|
|
5113
5228
|
static buildVectorSearchConditions(table, vectorSearch) {
|
|
5114
5229
|
const column = table[vectorSearch.property];
|
|
5115
5230
|
if (!column) throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
|
|
5231
|
+
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");
|
|
5116
5232
|
const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
|
|
5117
5233
|
const distanceFn = vectorSearch.distance || "cosine";
|
|
5118
5234
|
let operator;
|
|
@@ -5196,12 +5312,10 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5196
5312
|
continue;
|
|
5197
5313
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
5198
5314
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
5199
|
-
const pks = getPrimaryKeys(collection, registry);
|
|
5200
5315
|
inverseRelationUpdates.push({
|
|
5201
5316
|
relationKey: key,
|
|
5202
5317
|
relation,
|
|
5203
|
-
newValue: serializedValue
|
|
5204
|
-
currentId: row.id || buildCompositeId(row, pks)
|
|
5318
|
+
newValue: serializedValue
|
|
5205
5319
|
});
|
|
5206
5320
|
continue;
|
|
5207
5321
|
} else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
@@ -5211,15 +5325,11 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5211
5325
|
relation,
|
|
5212
5326
|
newTargetId: serializedValue
|
|
5213
5327
|
});
|
|
5214
|
-
else {
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
newValue: serializedValue,
|
|
5220
|
-
currentId: row.id || buildCompositeId(row, pks)
|
|
5221
|
-
});
|
|
5222
|
-
}
|
|
5328
|
+
else inverseRelationUpdates.push({
|
|
5329
|
+
relationKey: key,
|
|
5330
|
+
relation,
|
|
5331
|
+
newValue: serializedValue
|
|
5332
|
+
});
|
|
5223
5333
|
continue;
|
|
5224
5334
|
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
5225
5335
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
@@ -5619,6 +5729,63 @@ var RelationService = class {
|
|
|
5619
5729
|
this.registry = registry;
|
|
5620
5730
|
}
|
|
5621
5731
|
/**
|
|
5732
|
+
* One target row, as the {@link RelatedRow} everything here returns.
|
|
5733
|
+
*
|
|
5734
|
+
* Eight sites built this by hand, which is how the address came to be the
|
|
5735
|
+
* target's first key column in all eight — one edit, eight places to miss.
|
|
5736
|
+
*
|
|
5737
|
+
* `resolveNested` is the one thing they did not agree on, and the
|
|
5738
|
+
* disagreement was invisible: the single-parent fetches pass `db` and
|
|
5739
|
+
* `registry` to `parseDataFromServer`, so the target's *own* relations get
|
|
5740
|
+
* resolved too, while the batch paths deliberately do not — a query per
|
|
5741
|
+
* target row is the N+1 the batching exists to avoid. Naming the parameter
|
|
5742
|
+
* makes that a decision rather than a difference between two call sites
|
|
5743
|
+
* nobody was comparing.
|
|
5744
|
+
*/
|
|
5745
|
+
async toRelatedRow(targetRow, targetCollection, targetPks, options) {
|
|
5746
|
+
const values = options?.resolveNested ? await parseDataFromServer(targetRow, targetCollection, this.db, this.registry) : await parseDataFromServer(targetRow, targetCollection);
|
|
5747
|
+
return {
|
|
5748
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
5749
|
+
path: targetCollection.slug,
|
|
5750
|
+
values
|
|
5751
|
+
};
|
|
5752
|
+
}
|
|
5753
|
+
/**
|
|
5754
|
+
* A WHERE matching any of `parentIds`, by the whole key.
|
|
5755
|
+
*
|
|
5756
|
+
* A single key is an `IN (…)`. A composite one cannot be: matching
|
|
5757
|
+
* `tenant_id IN (1, 1)` collects every row of tenant 1, so two parents that
|
|
5758
|
+
* share their first column each receive the other's relations. It becomes
|
|
5759
|
+
* an OR of ANDs — one exact address per parent — which Postgres indexes the
|
|
5760
|
+
* same way it would a multi-column key lookup.
|
|
5761
|
+
*/
|
|
5762
|
+
parentKeyCondition(parentTable, parentPks, parentIds) {
|
|
5763
|
+
const columnFor = (fieldName) => {
|
|
5764
|
+
const col = parentTable[fieldName];
|
|
5765
|
+
if (!col) throw new Error(`Key column '${fieldName}' not found in parent table`);
|
|
5766
|
+
return col;
|
|
5767
|
+
};
|
|
5768
|
+
if (parentPks.length === 1) {
|
|
5769
|
+
const values = parentIds.map((id) => parseIdValues(id, parentPks)[parentPks[0].fieldName]);
|
|
5770
|
+
return inArray(columnFor(parentPks[0].fieldName), values);
|
|
5771
|
+
}
|
|
5772
|
+
return or(...parentIds.map((id) => {
|
|
5773
|
+
const values = parseIdValues(id, parentPks);
|
|
5774
|
+
return and(...parentPks.map((pk) => eq(columnFor(pk.fieldName), values[pk.fieldName])));
|
|
5775
|
+
}));
|
|
5776
|
+
}
|
|
5777
|
+
/**
|
|
5778
|
+
* Reject a relation that cannot express a composite-keyed parent.
|
|
5779
|
+
*
|
|
5780
|
+
* `localKey` and `foreignKeyOnTarget` are single column names: one column
|
|
5781
|
+
* cannot reference a two-column key, so such a relation has no correct
|
|
5782
|
+
* reading. Left alone it would silently match on the first key column and
|
|
5783
|
+
* hand a tenant's rows to its neighbour — say so instead.
|
|
5784
|
+
*/
|
|
5785
|
+
assertSingleKeyAddressable(parentCollection, parentPks, via) {
|
|
5786
|
+
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.`);
|
|
5787
|
+
}
|
|
5788
|
+
/**
|
|
5622
5789
|
* Fetch rows related to a parent row through a specific relation
|
|
5623
5790
|
*/
|
|
5624
5791
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5637,9 +5804,9 @@ var RelationService = class {
|
|
|
5637
5804
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5638
5805
|
const targetCollection = relation.target();
|
|
5639
5806
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5640
|
-
const idInfo =
|
|
5807
|
+
const idInfo = requirePrimaryKeys(targetCollection, this.registry);
|
|
5641
5808
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5642
|
-
const parentPks =
|
|
5809
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5643
5810
|
const parentIdInfo = parentPks[0];
|
|
5644
5811
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5645
5812
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5663,7 +5830,7 @@ var RelationService = class {
|
|
|
5663
5830
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5664
5831
|
currentTable = joinTable;
|
|
5665
5832
|
}
|
|
5666
|
-
const parentIdField = parentTable[
|
|
5833
|
+
const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5667
5834
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5668
5835
|
if (options.limit) query = query.limit(options.limit);
|
|
5669
5836
|
const results = await query;
|
|
@@ -5671,13 +5838,7 @@ var RelationService = class {
|
|
|
5671
5838
|
const rows = [];
|
|
5672
5839
|
for (const row of results) {
|
|
5673
5840
|
const targetRow = row[targetTableName] || row;
|
|
5674
|
-
|
|
5675
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5676
|
-
rows.push({
|
|
5677
|
-
id: id?.toString() || "",
|
|
5678
|
-
path: targetCollection.slug,
|
|
5679
|
-
values: parsedValues
|
|
5680
|
-
});
|
|
5841
|
+
rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
|
|
5681
5842
|
}
|
|
5682
5843
|
return rows;
|
|
5683
5844
|
}
|
|
@@ -5696,13 +5857,7 @@ var RelationService = class {
|
|
|
5696
5857
|
const rows = [];
|
|
5697
5858
|
for (const row of results) {
|
|
5698
5859
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5699
|
-
|
|
5700
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5701
|
-
rows.push({
|
|
5702
|
-
id: id?.toString() || "",
|
|
5703
|
-
path: targetCollection.slug,
|
|
5704
|
-
values: parsedValues
|
|
5705
|
-
});
|
|
5860
|
+
rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
|
|
5706
5861
|
}
|
|
5707
5862
|
return rows;
|
|
5708
5863
|
}
|
|
@@ -5719,8 +5874,8 @@ var RelationService = class {
|
|
|
5719
5874
|
}
|
|
5720
5875
|
const targetCollection = relation.target();
|
|
5721
5876
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5722
|
-
const targetIdField = targetTable[
|
|
5723
|
-
const parentPks =
|
|
5877
|
+
const targetIdField = targetTable[requirePrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5878
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5724
5879
|
const parentIdInfo = parentPks[0];
|
|
5725
5880
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5726
5881
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5739,9 +5894,10 @@ var RelationService = class {
|
|
|
5739
5894
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5740
5895
|
const targetCollection = relation.target();
|
|
5741
5896
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5742
|
-
const
|
|
5897
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
5898
|
+
const targetIdInfo = targetPks[0];
|
|
5743
5899
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5744
|
-
const parentPks =
|
|
5900
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5745
5901
|
const parentIdInfo = parentPks[0];
|
|
5746
5902
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5747
5903
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5765,25 +5921,19 @@ var RelationService = class {
|
|
|
5765
5921
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5766
5922
|
currentTable = joinTable;
|
|
5767
5923
|
}
|
|
5768
|
-
|
|
5769
|
-
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5924
|
+
query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
5770
5925
|
const results = await query;
|
|
5771
5926
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5772
5927
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5773
5928
|
for (const row of results) {
|
|
5774
5929
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5775
5930
|
const targetRow = row[targetTableName] || row;
|
|
5776
|
-
|
|
5777
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5778
|
-
resultMap.set(String(parentId), {
|
|
5779
|
-
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5780
|
-
path: targetCollection.slug,
|
|
5781
|
-
values: parsedValues
|
|
5782
|
-
});
|
|
5931
|
+
resultMap.set(buildCompositeId(parentRow, parentPks), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5783
5932
|
}
|
|
5784
5933
|
return resultMap;
|
|
5785
5934
|
}
|
|
5786
5935
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5936
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5787
5937
|
const localKeyCol = parentTable[relation.localKey];
|
|
5788
5938
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5789
5939
|
const fkRows = await this.db.select({
|
|
@@ -5812,17 +5962,11 @@ var RelationService = class {
|
|
|
5812
5962
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5813
5963
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
5814
5964
|
const targetRow = targetById.get(String(fkValue));
|
|
5815
|
-
if (targetRow)
|
|
5816
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5817
|
-
resultMap.set(parentIdStr, {
|
|
5818
|
-
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5819
|
-
path: targetCollection.slug,
|
|
5820
|
-
values: parsedValues
|
|
5821
|
-
});
|
|
5822
|
-
}
|
|
5965
|
+
if (targetRow) resultMap.set(parentIdStr, await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5823
5966
|
}
|
|
5824
5967
|
return resultMap;
|
|
5825
5968
|
}
|
|
5969
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
5826
5970
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
5827
5971
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5828
5972
|
const results = await query;
|
|
@@ -5833,14 +5977,7 @@ var RelationService = class {
|
|
|
5833
5977
|
let parentId;
|
|
5834
5978
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
5835
5979
|
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
5836
|
-
if (parentId !== void 0 && parentIdSet.has(String(parentId)))
|
|
5837
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5838
|
-
resultMap.set(String(parentId), {
|
|
5839
|
-
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5840
|
-
path: targetCollection.slug,
|
|
5841
|
-
values: parsedValues
|
|
5842
|
-
});
|
|
5843
|
-
}
|
|
5980
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5844
5981
|
}
|
|
5845
5982
|
return resultMap;
|
|
5846
5983
|
}
|
|
@@ -5854,9 +5991,9 @@ var RelationService = class {
|
|
|
5854
5991
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5855
5992
|
const targetCollection = relation.target();
|
|
5856
5993
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5857
|
-
const
|
|
5858
|
-
const targetIdField = targetTable[
|
|
5859
|
-
const parentPks =
|
|
5994
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
5995
|
+
const targetIdField = targetTable[targetPks[0].fieldName];
|
|
5996
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5860
5997
|
const parentIdInfo = parentPks[0];
|
|
5861
5998
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5862
5999
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5878,27 +6015,22 @@ var RelationService = class {
|
|
|
5878
6015
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5879
6016
|
currentTable = joinTable;
|
|
5880
6017
|
}
|
|
5881
|
-
|
|
5882
|
-
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6018
|
+
query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
5883
6019
|
const results = await query;
|
|
5884
6020
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5885
6021
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5886
6022
|
for (const row of results) {
|
|
5887
6023
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5888
6024
|
const targetRow = row[targetTableName] || row;
|
|
5889
|
-
const parentId =
|
|
5890
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6025
|
+
const parentId = buildCompositeId(parentRow, parentPks);
|
|
5891
6026
|
const arr = resultMap.get(parentId) || [];
|
|
5892
|
-
arr.push(
|
|
5893
|
-
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5894
|
-
path: targetCollection.slug,
|
|
5895
|
-
values: parsedValues
|
|
5896
|
-
});
|
|
6027
|
+
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5897
6028
|
resultMap.set(parentId, arr);
|
|
5898
6029
|
}
|
|
5899
6030
|
return resultMap;
|
|
5900
6031
|
}
|
|
5901
6032
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6033
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
5902
6034
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
5903
6035
|
if (!junctionTable) {
|
|
5904
6036
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -5917,17 +6049,13 @@ var RelationService = class {
|
|
|
5917
6049
|
const junctionData = row[relation.through.table] || row;
|
|
5918
6050
|
const targetData = row[targetTableName] || row;
|
|
5919
6051
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
5920
|
-
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
5921
6052
|
const arr = resultMap.get(parentId) || [];
|
|
5922
|
-
arr.push(
|
|
5923
|
-
id: String(targetData[targetIdInfo.fieldName]),
|
|
5924
|
-
path: targetCollection.slug,
|
|
5925
|
-
values: parsedValues
|
|
5926
|
-
});
|
|
6053
|
+
arr.push(await this.toRelatedRow(targetData, targetCollection, targetPks));
|
|
5927
6054
|
resultMap.set(parentId, arr);
|
|
5928
6055
|
}
|
|
5929
6056
|
return resultMap;
|
|
5930
6057
|
}
|
|
6058
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
5931
6059
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
5932
6060
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5933
6061
|
const results = await query;
|
|
@@ -5940,14 +6068,9 @@ var RelationService = class {
|
|
|
5940
6068
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
5941
6069
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
5942
6070
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5943
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5944
6071
|
const key = String(parentId);
|
|
5945
6072
|
const arr = resultMap.get(key) || [];
|
|
5946
|
-
arr.push(
|
|
5947
|
-
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5948
|
-
path: targetCollection.slug,
|
|
5949
|
-
values: parsedValues
|
|
5950
|
-
});
|
|
6073
|
+
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5951
6074
|
resultMap.set(key, arr);
|
|
5952
6075
|
}
|
|
5953
6076
|
}
|
|
@@ -5995,12 +6118,12 @@ var RelationService = class {
|
|
|
5995
6118
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
5996
6119
|
continue;
|
|
5997
6120
|
}
|
|
5998
|
-
const parentPks =
|
|
6121
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
5999
6122
|
const parentIdInfo = parentPks[0];
|
|
6000
6123
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6001
6124
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6002
6125
|
if (targetEntityIds.length > 0) {
|
|
6003
|
-
const targetPks =
|
|
6126
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6004
6127
|
const targetIdInfo = targetPks[0];
|
|
6005
6128
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6006
6129
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6020,12 +6143,12 @@ var RelationService = class {
|
|
|
6020
6143
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6021
6144
|
continue;
|
|
6022
6145
|
}
|
|
6023
|
-
const parentPks =
|
|
6146
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6024
6147
|
const parentIdInfo = parentPks[0];
|
|
6025
6148
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6026
6149
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6027
6150
|
if (targetEntityIds.length > 0) {
|
|
6028
|
-
const targetPks =
|
|
6151
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6029
6152
|
const targetIdInfo = targetPks[0];
|
|
6030
6153
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6031
6154
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6036,7 +6159,7 @@ var RelationService = class {
|
|
|
6036
6159
|
} 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.`);
|
|
6037
6160
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6038
6161
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6039
|
-
const targetPks =
|
|
6162
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6040
6163
|
const targetIdInfo = targetPks[0];
|
|
6041
6164
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6042
6165
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6044,7 +6167,7 @@ var RelationService = class {
|
|
|
6044
6167
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6045
6168
|
continue;
|
|
6046
6169
|
}
|
|
6047
|
-
const parentPks =
|
|
6170
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6048
6171
|
const parentIdInfo = parentPks[0];
|
|
6049
6172
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6050
6173
|
if (targetEntityIds.length > 0) {
|
|
@@ -6064,9 +6187,9 @@ var RelationService = class {
|
|
|
6064
6187
|
try {
|
|
6065
6188
|
const targetCollection = relation.target();
|
|
6066
6189
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6067
|
-
const targetPks =
|
|
6190
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6068
6191
|
const targetIdInfo = targetPks[0];
|
|
6069
|
-
const sourcePks =
|
|
6192
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6070
6193
|
const sourceIdInfo = sourcePks[0];
|
|
6071
6194
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6072
6195
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6147,12 +6270,12 @@ var RelationService = class {
|
|
|
6147
6270
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6148
6271
|
return;
|
|
6149
6272
|
}
|
|
6150
|
-
const sourcePks =
|
|
6273
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6151
6274
|
const sourceIdInfo = sourcePks[0];
|
|
6152
6275
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6153
6276
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6154
6277
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6155
|
-
const targetPks =
|
|
6278
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6156
6279
|
const targetIdInfo = targetPks[0];
|
|
6157
6280
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6158
6281
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6160,7 +6283,7 @@ var RelationService = class {
|
|
|
6160
6283
|
}));
|
|
6161
6284
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6162
6285
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6163
|
-
const targetPks =
|
|
6286
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6164
6287
|
const targetIdInfo = targetPks[0];
|
|
6165
6288
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6166
6289
|
const newLink = {
|
|
@@ -6191,12 +6314,12 @@ var RelationService = class {
|
|
|
6191
6314
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6192
6315
|
return;
|
|
6193
6316
|
}
|
|
6194
|
-
const sourcePks =
|
|
6317
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6195
6318
|
const sourceIdInfo = sourcePks[0];
|
|
6196
6319
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6197
6320
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6198
6321
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6199
|
-
const targetPks =
|
|
6322
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6200
6323
|
const targetIdInfo = targetPks[0];
|
|
6201
6324
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6202
6325
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6217,12 +6340,12 @@ var RelationService = class {
|
|
|
6217
6340
|
const { relation, newTargetId } = upd;
|
|
6218
6341
|
const targetCollection = relation.target();
|
|
6219
6342
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6220
|
-
const targetPks =
|
|
6343
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6221
6344
|
const targetIdInfo = targetPks[0];
|
|
6222
6345
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6223
6346
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6224
6347
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6225
|
-
const parentPks =
|
|
6348
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
6226
6349
|
const parentIdInfo = parentPks[0];
|
|
6227
6350
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6228
6351
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6293,7 +6416,7 @@ var RelationService = class {
|
|
|
6293
6416
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6294
6417
|
return;
|
|
6295
6418
|
}
|
|
6296
|
-
const targetPks =
|
|
6419
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6297
6420
|
const targetIdInfo = targetPks[0];
|
|
6298
6421
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6299
6422
|
const junctionData = {
|
|
@@ -6309,6 +6432,131 @@ var RelationService = class {
|
|
|
6309
6432
|
}
|
|
6310
6433
|
};
|
|
6311
6434
|
//#endregion
|
|
6435
|
+
//#region src/services/row-pipeline.ts
|
|
6436
|
+
/**
|
|
6437
|
+
* Whether a many-relation reaches its target through a junction table.
|
|
6438
|
+
*
|
|
6439
|
+
* Also used to build the drizzle `with` config, which is why it is exported:
|
|
6440
|
+
* the query has to nest one level deeper for a junction, and the row walk has
|
|
6441
|
+
* to unwrap that same level back out.
|
|
6442
|
+
*/
|
|
6443
|
+
function isJunctionRelation(relation) {
|
|
6444
|
+
if (relation.through) return true;
|
|
6445
|
+
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6446
|
+
return false;
|
|
6447
|
+
}
|
|
6448
|
+
/**
|
|
6449
|
+
* Reach the target row inside a junction row.
|
|
6450
|
+
*
|
|
6451
|
+
* A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
|
|
6452
|
+
* the foreign keys, and the target nested under one of them. The target is the
|
|
6453
|
+
* only object among them, so that is how it is found. A junction row that has
|
|
6454
|
+
* not been nested (no `with` on the join) has no object and is returned as-is.
|
|
6455
|
+
*/
|
|
6456
|
+
function unwrapJunctionRow(item) {
|
|
6457
|
+
const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
|
|
6458
|
+
return nestedKey ? item[nestedKey] : item;
|
|
6459
|
+
}
|
|
6460
|
+
/**
|
|
6461
|
+
* Give back the number a `number` property was declared to be.
|
|
6462
|
+
*
|
|
6463
|
+
* Postgres returns NUMERIC as a string and drizzle keeps it that way, since a
|
|
6464
|
+
* numeric can hold more precision than a double. But the collection declared
|
|
6465
|
+
* this property `number` and the OpenAPI spec this server publishes says
|
|
6466
|
+
* `type: number`, so serving `"9.99"` breaks its own contract — and breaks it
|
|
6467
|
+
* asymmetrically, because a create answers with the number and the read that
|
|
6468
|
+
* follows answers with the string. Any client that multiplies a price works
|
|
6469
|
+
* until the first refresh.
|
|
6470
|
+
*
|
|
6471
|
+
* Declaring a property `number` already accepts double precision — that is what
|
|
6472
|
+
* the admin has always parsed it to. Columns REST serves that no property
|
|
6473
|
+
* declares are left exactly as the database returned them.
|
|
6474
|
+
*/
|
|
6475
|
+
function coerceDeclaredNumber(value, property) {
|
|
6476
|
+
if (property?.type !== "number" || typeof value !== "string") return value;
|
|
6477
|
+
const parsed = parseFloat(value);
|
|
6478
|
+
return isNaN(parsed) ? null : parsed;
|
|
6479
|
+
}
|
|
6480
|
+
/** Apply {@link coerceDeclaredNumber} across a row, leaving every other column alone. */
|
|
6481
|
+
function coerceDeclaredNumbers(row, collection) {
|
|
6482
|
+
const properties = collection.properties;
|
|
6483
|
+
if (!properties) return row;
|
|
6484
|
+
const out = {};
|
|
6485
|
+
for (const [key, value] of Object.entries(row)) out[key] = coerceDeclaredNumber(value, properties[key]);
|
|
6486
|
+
return out;
|
|
6487
|
+
}
|
|
6488
|
+
/** Render one target row in the requested style. */
|
|
6489
|
+
function renderTarget(targetRow, targetCollection, style, registry) {
|
|
6490
|
+
if (style === "inline") return coerceDeclaredNumbers({ ...targetRow }, targetCollection);
|
|
6491
|
+
const address = relationTargetAddress(targetRow, targetCollection, registry);
|
|
6492
|
+
const path = targetCollection.slug;
|
|
6493
|
+
return createRelationRefWithData(address, path, {
|
|
6494
|
+
id: address,
|
|
6495
|
+
path,
|
|
6496
|
+
values: normalizeDbValues(targetRow, targetCollection)
|
|
6497
|
+
});
|
|
6498
|
+
}
|
|
6499
|
+
/**
|
|
6500
|
+
* The address a relation ref points at.
|
|
6501
|
+
*
|
|
6502
|
+
* The whole key, not its first column: a composite-keyed target addressed by
|
|
6503
|
+
* `tenant_id` alone points at every row that shares it. A target whose key
|
|
6504
|
+
* cannot be resolved at all used to throw here — reading `[0]` of an empty
|
|
6505
|
+
* array — taking down the parent's fetch over a relation it may not even have
|
|
6506
|
+
* asked for. The first column is a guess, but a ref that resolves to nothing
|
|
6507
|
+
* beats no rows at all.
|
|
6508
|
+
*/
|
|
6509
|
+
function relationTargetAddress(targetRow, targetCollection, registry) {
|
|
6510
|
+
const address = deriveRowAddress(targetRow, targetCollection, registry);
|
|
6511
|
+
if (address) return address;
|
|
6512
|
+
return String(targetRow[Object.keys(targetRow)[0]] ?? "");
|
|
6513
|
+
}
|
|
6514
|
+
/**
|
|
6515
|
+
* The row the admin renders: every column, with relations as references.
|
|
6516
|
+
*
|
|
6517
|
+
* Values are normalized (dates, numbers, NaN) because the admin's view-model
|
|
6518
|
+
* expects real types. The row's own address is *not* among the columns — it is
|
|
6519
|
+
* derived by the consumer from the collection's primary keys.
|
|
6520
|
+
*/
|
|
6521
|
+
function toCmsRow(row, collection, registry) {
|
|
6522
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6523
|
+
const normalized = normalizeDbValues(row, collection);
|
|
6524
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6525
|
+
const relData = row[relation.relationName || key];
|
|
6526
|
+
if (relData === void 0 || relData === null) continue;
|
|
6527
|
+
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6528
|
+
const targetCollection = relation.target();
|
|
6529
|
+
normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
|
|
6530
|
+
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
|
|
6531
|
+
}
|
|
6532
|
+
return normalized;
|
|
6533
|
+
}
|
|
6534
|
+
/**
|
|
6535
|
+
* The row REST serves: every column under its own name, with the value Postgres
|
|
6536
|
+
* returned, and relations inlined as the target's columns.
|
|
6537
|
+
*
|
|
6538
|
+
* Values are the ones the database returned, except where that contradicts the
|
|
6539
|
+
* declared type: a `number` property is served as a number (see
|
|
6540
|
+
* {@link coerceDeclaredNumber}). Dates stay as the database returned them —
|
|
6541
|
+
* JSON has its own opinions about dates that the admin's view-model does not
|
|
6542
|
+
* share.
|
|
6543
|
+
*
|
|
6544
|
+
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
6545
|
+
* the relations `include` asked for, so the row is the authority on which are
|
|
6546
|
+
* actually there.
|
|
6547
|
+
*/
|
|
6548
|
+
function toRestRow(row, collection, registry) {
|
|
6549
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6550
|
+
const flat = {};
|
|
6551
|
+
for (const [key, value] of Object.entries(row)) {
|
|
6552
|
+
const relation = findRelation(resolvedRelations, key);
|
|
6553
|
+
if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
|
|
6554
|
+
else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
|
|
6555
|
+
else flat[key] = coerceDeclaredNumber(value, collection.properties?.[key]);
|
|
6556
|
+
}
|
|
6557
|
+
return flat;
|
|
6558
|
+
}
|
|
6559
|
+
//#endregion
|
|
6312
6560
|
//#region src/services/FetchService.ts
|
|
6313
6561
|
/**
|
|
6314
6562
|
* Service for handling all row read operations.
|
|
@@ -6367,7 +6615,7 @@ var FetchService = class {
|
|
|
6367
6615
|
if (!shouldInclude(key)) continue;
|
|
6368
6616
|
const drizzleRelName = relation.relationName || key;
|
|
6369
6617
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6370
|
-
if (relation.cardinality === "many" &&
|
|
6618
|
+
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
6371
6619
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6372
6620
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6373
6621
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6376,14 +6624,6 @@ var FetchService = class {
|
|
|
6376
6624
|
return withConfig;
|
|
6377
6625
|
}
|
|
6378
6626
|
/**
|
|
6379
|
-
* Detect if a many-to-many relation uses a junction table in the Drizzle schema.
|
|
6380
|
-
*/
|
|
6381
|
-
isJunctionRelation(relation, _collection) {
|
|
6382
|
-
if (relation.through) return true;
|
|
6383
|
-
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6384
|
-
return false;
|
|
6385
|
-
}
|
|
6386
|
-
/**
|
|
6387
6627
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6388
6628
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6389
6629
|
*/
|
|
@@ -6392,54 +6632,6 @@ var FetchService = class {
|
|
|
6392
6632
|
return null;
|
|
6393
6633
|
}
|
|
6394
6634
|
/**
|
|
6395
|
-
* Convert a db.query result row (with nested relation objects) to a flat row.
|
|
6396
|
-
* Handles:
|
|
6397
|
-
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
6398
|
-
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
6399
|
-
* - Flattening junction-table many-to-many results
|
|
6400
|
-
*
|
|
6401
|
-
* The row's own address is not among them: it is derived by the consumer
|
|
6402
|
-
* from the collection's primary keys.
|
|
6403
|
-
*/
|
|
6404
|
-
drizzleResultToRow(row, collection) {
|
|
6405
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6406
|
-
const normalizedValues = normalizeDbValues(row, collection);
|
|
6407
|
-
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6408
|
-
const relData = row[relation.relationName || key];
|
|
6409
|
-
if (relData === void 0 || relData === null) continue;
|
|
6410
|
-
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6411
|
-
const targetCollection = relation.target();
|
|
6412
|
-
const targetPath = targetCollection.slug;
|
|
6413
|
-
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
6414
|
-
normalizedValues[key] = relData.map((item) => {
|
|
6415
|
-
let targetRow = item;
|
|
6416
|
-
if (this.isJunctionRelation(relation, collection)) {
|
|
6417
|
-
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6418
|
-
if (nestedKey) targetRow = item[nestedKey];
|
|
6419
|
-
}
|
|
6420
|
-
const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
|
|
6421
|
-
return createRelationRefWithData(relId, targetPath, {
|
|
6422
|
-
id: relId,
|
|
6423
|
-
path: targetPath,
|
|
6424
|
-
values: normalizeDbValues(targetRow, targetCollection)
|
|
6425
|
-
});
|
|
6426
|
-
});
|
|
6427
|
-
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
6428
|
-
const targetCollection = relation.target();
|
|
6429
|
-
const targetPath = targetCollection.slug;
|
|
6430
|
-
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
6431
|
-
const relObj = relData;
|
|
6432
|
-
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
6433
|
-
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
6434
|
-
id: relId,
|
|
6435
|
-
path: targetPath,
|
|
6436
|
-
values: normalizeDbValues(relObj, targetCollection)
|
|
6437
|
-
});
|
|
6438
|
-
}
|
|
6439
|
-
}
|
|
6440
|
-
return normalizedValues;
|
|
6441
|
-
}
|
|
6442
|
-
/**
|
|
6443
6635
|
* Post-fetch joinPath relations for a single flat row.
|
|
6444
6636
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6445
6637
|
* so they must be loaded separately after the primary query.
|
|
@@ -6460,31 +6652,6 @@ var FetchService = class {
|
|
|
6460
6652
|
await Promise.all(promises);
|
|
6461
6653
|
}
|
|
6462
6654
|
/**
|
|
6463
|
-
* Post-fetch joinPath relations for a batch of flat rows.
|
|
6464
|
-
* Uses batch fetching to avoid N+1 queries for list views.
|
|
6465
|
-
*/
|
|
6466
|
-
async resolveJoinPathRelationsBatch(rows, collection, collectionPath, idInfo, _databaseId) {
|
|
6467
|
-
if (rows.length === 0) return;
|
|
6468
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6469
|
-
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
6470
|
-
if (joinPathRelations.length === 0) return;
|
|
6471
|
-
for (const [key, relation] of joinPathRelations) try {
|
|
6472
|
-
const rowIds = rows.map((r) => {
|
|
6473
|
-
return parseIdValues(String(r.id), [idInfo])[idInfo.fieldName];
|
|
6474
|
-
});
|
|
6475
|
-
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6476
|
-
for (const row of rows) {
|
|
6477
|
-
const id = parseIdValues(String(row.id), [idInfo])[idInfo.fieldName];
|
|
6478
|
-
const relatedRow = resultMap.get(String(id));
|
|
6479
|
-
if (relatedRow) {
|
|
6480
|
-
if (relation.cardinality === "one") row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
|
|
6481
|
-
}
|
|
6482
|
-
}
|
|
6483
|
-
} catch (e) {
|
|
6484
|
-
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
6485
|
-
}
|
|
6486
|
-
}
|
|
6487
|
-
/**
|
|
6488
6655
|
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
6489
6656
|
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
6490
6657
|
*/
|
|
@@ -6495,63 +6662,29 @@ var FetchService = class {
|
|
|
6495
6662
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6496
6663
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6497
6664
|
if (joinPathRelations.length === 0) return;
|
|
6498
|
-
const
|
|
6665
|
+
const parentIdOf = (row) => {
|
|
6666
|
+
const address = buildCompositeId(row, idInfoArray);
|
|
6667
|
+
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6668
|
+
};
|
|
6499
6669
|
for (const [key, relation] of joinPathRelations) try {
|
|
6500
|
-
const
|
|
6501
|
-
|
|
6502
|
-
|
|
6670
|
+
const addressable = rows.filter((r) => parentIdOf(r) !== void 0 && parentIdOf(r) !== null);
|
|
6671
|
+
if (addressable.length === 0) continue;
|
|
6672
|
+
const rowIds = addressable.map((r) => parentIdOf(r));
|
|
6503
6673
|
if (relation.cardinality === "one") {
|
|
6504
6674
|
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6505
|
-
for (const row of
|
|
6506
|
-
const
|
|
6507
|
-
|
|
6508
|
-
if (relatedRow) row[key] = {
|
|
6509
|
-
...relatedRow.values,
|
|
6510
|
-
id: relatedRow.id
|
|
6511
|
-
};
|
|
6512
|
-
else row[key] = null;
|
|
6675
|
+
for (const row of addressable) {
|
|
6676
|
+
const relatedRow = resultMap.get(String(parentIdOf(row)));
|
|
6677
|
+
row[key] = relatedRow ? { ...relatedRow.values } : null;
|
|
6513
6678
|
}
|
|
6514
6679
|
} else if (relation.cardinality === "many") {
|
|
6515
6680
|
const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, rowIds, key, relation);
|
|
6516
|
-
for (const row of
|
|
6517
|
-
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
6518
|
-
row[key] = (resultMap.get(String(id)) || []).map((e) => ({
|
|
6519
|
-
...e.values,
|
|
6520
|
-
id: e.id
|
|
6521
|
-
}));
|
|
6522
|
-
}
|
|
6681
|
+
for (const row of addressable) row[key] = (resultMap.get(String(parentIdOf(row))) || []).map((e) => ({ ...e.values }));
|
|
6523
6682
|
}
|
|
6524
6683
|
} catch (e) {
|
|
6525
6684
|
logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
|
|
6526
6685
|
}
|
|
6527
6686
|
}
|
|
6528
6687
|
/**
|
|
6529
|
-
* Convert a db.query result row to a flat REST-style row with populated relations.
|
|
6530
|
-
*
|
|
6531
|
-
* Every column is copied through under its own name, with the value Postgres
|
|
6532
|
-
* returned. This used to open with a synthesized `id` and then skip the key
|
|
6533
|
-
* column, which renamed it (a `sku` primary key was served as `id`, and `sku`
|
|
6534
|
-
* did not appear at all) and restringified it (`42` → `"42"`). Consumers that
|
|
6535
|
-
* need an address derive it from the collection's primary keys.
|
|
6536
|
-
*/
|
|
6537
|
-
drizzleResultToRestRow(row, collection) {
|
|
6538
|
-
const flat = {};
|
|
6539
|
-
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6540
|
-
for (const [k, v] of Object.entries(row)) {
|
|
6541
|
-
const relation = findRelation(resolvedRelations, k);
|
|
6542
|
-
if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
|
|
6543
|
-
if (this.isJunctionRelation(relation, collection)) {
|
|
6544
|
-
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6545
|
-
if (nestedKey) return { ...item[nestedKey] };
|
|
6546
|
-
}
|
|
6547
|
-
return { ...item };
|
|
6548
|
-
});
|
|
6549
|
-
else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) flat[k] = { ...v };
|
|
6550
|
-
else flat[k] = v;
|
|
6551
|
-
}
|
|
6552
|
-
return flat;
|
|
6553
|
-
}
|
|
6554
|
-
/**
|
|
6555
6688
|
* Build db.query-compatible options from standard fetch options.
|
|
6556
6689
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6557
6690
|
*/
|
|
@@ -6621,7 +6754,7 @@ var FetchService = class {
|
|
|
6621
6754
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6622
6755
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6623
6756
|
const table = getTableForCollection(collection, this.registry);
|
|
6624
|
-
const idInfoArray =
|
|
6757
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6625
6758
|
const idInfo = idInfoArray[0];
|
|
6626
6759
|
const idField = table[idInfo.fieldName];
|
|
6627
6760
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6635,7 +6768,7 @@ var FetchService = class {
|
|
|
6635
6768
|
with: withConfig
|
|
6636
6769
|
});
|
|
6637
6770
|
if (!row) return void 0;
|
|
6638
|
-
const flatRow =
|
|
6771
|
+
const flatRow = toCmsRow(row, collection, this.registry);
|
|
6639
6772
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6640
6773
|
return flatRow;
|
|
6641
6774
|
} catch (e) {
|
|
@@ -6677,7 +6810,7 @@ var FetchService = class {
|
|
|
6677
6810
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6678
6811
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6679
6812
|
const table = getTableForCollection(collection, this.registry);
|
|
6680
|
-
const idInfoArray =
|
|
6813
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6681
6814
|
const idInfo = idInfoArray[0];
|
|
6682
6815
|
const idField = table[idInfo.fieldName];
|
|
6683
6816
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6687,7 +6820,7 @@ var FetchService = class {
|
|
|
6687
6820
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6688
6821
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6689
6822
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6690
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6823
|
+
return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
|
|
6691
6824
|
} catch (e) {
|
|
6692
6825
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6693
6826
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6748,8 +6881,10 @@ var FetchService = class {
|
|
|
6748
6881
|
}
|
|
6749
6882
|
/**
|
|
6750
6883
|
* Fallback path used when db.query is unavailable.
|
|
6751
|
-
*
|
|
6752
|
-
*
|
|
6884
|
+
*
|
|
6885
|
+
* The primary path runs the results through `toCmsRow`, which maps
|
|
6886
|
+
* relations from what drizzle already nested — no query per row. This one
|
|
6887
|
+
* has no nesting to read, so it resolves relations itself, in batches.
|
|
6753
6888
|
*
|
|
6754
6889
|
* Process raw database results into flat rows with relations.
|
|
6755
6890
|
*/
|
|
@@ -6829,10 +6964,7 @@ var FetchService = class {
|
|
|
6829
6964
|
const relationKey = pathSegments[i];
|
|
6830
6965
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
6831
6966
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
6832
|
-
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6833
|
-
...row.values,
|
|
6834
|
-
id: row.id
|
|
6835
|
-
}));
|
|
6967
|
+
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({ ...row.values }));
|
|
6836
6968
|
if (i + 1 < pathSegments.length) {
|
|
6837
6969
|
const nextEntityId = pathSegments[i + 1];
|
|
6838
6970
|
currentCollection = relation.target();
|
|
@@ -6894,7 +7026,7 @@ var FetchService = class {
|
|
|
6894
7026
|
if (value === void 0 || value === null) return true;
|
|
6895
7027
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6896
7028
|
const table = getTableForCollection(collection, this.registry);
|
|
6897
|
-
const idInfoArray =
|
|
7029
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6898
7030
|
const idInfo = idInfoArray[0];
|
|
6899
7031
|
const idField = table[idInfo.fieldName];
|
|
6900
7032
|
const field = table[fieldName];
|
|
@@ -6921,7 +7053,7 @@ var FetchService = class {
|
|
|
6921
7053
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
6922
7054
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6923
7055
|
const table = getTableForCollection(collection, this.registry);
|
|
6924
|
-
const idInfoArray =
|
|
7056
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6925
7057
|
const idInfo = idInfoArray[0];
|
|
6926
7058
|
const idField = table[idInfo.fieldName];
|
|
6927
7059
|
const tableName = getTableName(table);
|
|
@@ -6929,7 +7061,7 @@ var FetchService = class {
|
|
|
6929
7061
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
6930
7062
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
6931
7063
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
6932
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
7064
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => toRestRow(row, collection, this.registry));
|
|
6933
7065
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
6934
7066
|
return restRows;
|
|
6935
7067
|
} catch (e) {
|
|
@@ -6978,7 +7110,7 @@ var FetchService = class {
|
|
|
6978
7110
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
6979
7111
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6980
7112
|
const table = getTableForCollection(collection, this.registry);
|
|
6981
|
-
const idInfoArray =
|
|
7113
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6982
7114
|
const idInfo = idInfoArray[0];
|
|
6983
7115
|
const idField = table[idInfo.fieldName];
|
|
6984
7116
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -6991,7 +7123,7 @@ var FetchService = class {
|
|
|
6991
7123
|
...withConfig ? { with: withConfig } : {}
|
|
6992
7124
|
});
|
|
6993
7125
|
if (!row) return null;
|
|
6994
|
-
const restRow =
|
|
7126
|
+
const restRow = toRestRow(row, collection, this.registry);
|
|
6995
7127
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
6996
7128
|
return restRow;
|
|
6997
7129
|
} catch (e) {
|
|
@@ -7036,7 +7168,7 @@ var FetchService = class {
|
|
|
7036
7168
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7037
7169
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7038
7170
|
const table = getTableForCollection(collection, this.registry);
|
|
7039
|
-
const idField = table[
|
|
7171
|
+
const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7040
7172
|
let vectorMeta;
|
|
7041
7173
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7042
7174
|
let query = vectorMeta ? this.db.select({
|
|
@@ -7525,7 +7657,7 @@ var PersistService = class {
|
|
|
7525
7657
|
} catch (error) {
|
|
7526
7658
|
throw this.toUserFriendlyError(error, collection.slug);
|
|
7527
7659
|
}
|
|
7528
|
-
const finalEntity = await this.fetchService.
|
|
7660
|
+
const finalEntity = await this.fetchService.fetchOneForRest(collection.slug, savedId, void 0, databaseId);
|
|
7529
7661
|
if (!finalEntity) throw new Error("Could not fetch row after save.");
|
|
7530
7662
|
return finalEntity;
|
|
7531
7663
|
}
|
|
@@ -8444,12 +8576,14 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8444
8576
|
let updatedValues = values;
|
|
8445
8577
|
const contextForCallback = this.buildCallContext();
|
|
8446
8578
|
let previousValuesForHistory;
|
|
8447
|
-
if (status === "existing" && id) {
|
|
8448
|
-
const existing = await this.dataService.
|
|
8579
|
+
if (status === "existing" && id) try {
|
|
8580
|
+
const existing = await this.dataService.getFetchService().fetchOneForRest(path, id, void 0, resolvedCollection?.databaseId);
|
|
8449
8581
|
if (existing) {
|
|
8450
8582
|
const { id: _existingId, ...existingValues } = existing;
|
|
8451
8583
|
previousValuesForHistory = existingValues;
|
|
8452
8584
|
}
|
|
8585
|
+
} catch (err) {
|
|
8586
|
+
logger.debug(`[save] Could not fetch previous values for "${path}"`, { detail: err instanceof Error ? err.message : String(err) });
|
|
8453
8587
|
}
|
|
8454
8588
|
if (globalCallbacks?.beforeSave || callbacks?.beforeSave || propertyCallbacks?.beforeSave) {
|
|
8455
8589
|
if (globalCallbacks?.beforeSave) {
|
|
@@ -8517,8 +8651,8 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8517
8651
|
context: contextForCallback
|
|
8518
8652
|
});
|
|
8519
8653
|
}
|
|
8520
|
-
const savedId = savedRow.
|
|
8521
|
-
const
|
|
8654
|
+
const savedId = deriveRowAddress(savedRow, resolvedCollection ?? collection, this.registry);
|
|
8655
|
+
const savedValues = savedRow;
|
|
8522
8656
|
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
8523
8657
|
if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
|
|
8524
8658
|
collection: resolvedCollection,
|
|
@@ -8550,7 +8684,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8550
8684
|
}
|
|
8551
8685
|
if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
|
|
8552
8686
|
tableName: path,
|
|
8553
|
-
id: savedId
|
|
8687
|
+
id: savedId,
|
|
8554
8688
|
action: status === "new" ? "create" : "update",
|
|
8555
8689
|
values: savedValues,
|
|
8556
8690
|
previousValues: previousValuesForHistory,
|
|
@@ -8558,11 +8692,11 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8558
8692
|
});
|
|
8559
8693
|
if (this._deferNotifications) this._pendingNotifications.push({
|
|
8560
8694
|
path,
|
|
8561
|
-
id: savedId
|
|
8695
|
+
id: savedId,
|
|
8562
8696
|
row: savedRow,
|
|
8563
8697
|
databaseId: resolvedCollection?.databaseId
|
|
8564
8698
|
});
|
|
8565
|
-
else await this.realtimeService.notifyUpdate(path, savedId
|
|
8699
|
+
else await this.realtimeService.notifyUpdate(path, savedId, savedRow, resolvedCollection?.databaseId);
|
|
8566
8700
|
return savedRow;
|
|
8567
8701
|
} catch (error) {
|
|
8568
8702
|
if (globalCallbacks?.afterSaveError || callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
@@ -8642,10 +8776,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8642
8776
|
}
|
|
8643
8777
|
async delete({ row, collection }) {
|
|
8644
8778
|
const targetPath = row.path;
|
|
8645
|
-
const targetRow = {
|
|
8646
|
-
id: row.id,
|
|
8647
|
-
...row.values ?? {}
|
|
8648
|
-
};
|
|
8779
|
+
const targetRow = { ...row.values ?? {} };
|
|
8649
8780
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, targetPath);
|
|
8650
8781
|
const contextForCallback = this.buildCallContext();
|
|
8651
8782
|
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
@@ -10328,7 +10459,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10328
10459
|
startAfter: request.startAfter,
|
|
10329
10460
|
searchString: request.searchString
|
|
10330
10461
|
}, authContext);
|
|
10331
|
-
this.sendCollectionUpdate(clientId, subscriptionId, rows);
|
|
10462
|
+
this.sendCollectionUpdate(clientId, subscriptionId, rows, request.path);
|
|
10332
10463
|
} catch (error) {
|
|
10333
10464
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
10334
10465
|
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10429,7 +10560,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10429
10560
|
if (subscription.type === "single" && notifyPath === originalPath) if (row && row?._rebase_invalidated) this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);
|
|
10430
10561
|
else this.sendSingleUpdate(subscription.clientId, subscriptionId, row);
|
|
10431
10562
|
else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
10432
|
-
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row);
|
|
10563
|
+
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row, notifyPath);
|
|
10433
10564
|
this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
|
|
10434
10565
|
}
|
|
10435
10566
|
} catch (error) {
|
|
@@ -10459,7 +10590,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10459
10590
|
if (!this._subscriptions.has(subscriptionId)) return;
|
|
10460
10591
|
try {
|
|
10461
10592
|
const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
|
|
10462
|
-
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows);
|
|
10593
|
+
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows, notifyPath);
|
|
10463
10594
|
} catch (error) {
|
|
10464
10595
|
const sanitized = sanitizeErrorForClient(error, notifyPath);
|
|
10465
10596
|
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10673,11 +10804,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10673
10804
|
}
|
|
10674
10805
|
return await this.dataService.fetchOne(notifyPath, id);
|
|
10675
10806
|
}
|
|
10676
|
-
sendCollectionUpdate(clientId, subscriptionId, rows) {
|
|
10807
|
+
sendCollectionUpdate(clientId, subscriptionId, rows, path) {
|
|
10677
10808
|
const message = {
|
|
10678
10809
|
type: "collection_update",
|
|
10679
10810
|
subscriptionId,
|
|
10680
|
-
rows
|
|
10811
|
+
rows,
|
|
10812
|
+
pks: this.primaryKeysForPath(path)
|
|
10681
10813
|
};
|
|
10682
10814
|
this.sendMessage(clientId, message);
|
|
10683
10815
|
}
|
|
@@ -10692,16 +10824,33 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10692
10824
|
/**
|
|
10693
10825
|
* Send a lightweight row-level patch to a collection subscriber.
|
|
10694
10826
|
* The client can merge this into its cached data for instant feedback.
|
|
10827
|
+
*
|
|
10828
|
+
* The key columns ride along: the patch names a row by address, and the
|
|
10829
|
+
* client has to find that row among the ones it cached — which carry
|
|
10830
|
+
* columns and no address. The SDK holds no collection config to derive one
|
|
10831
|
+
* from, so this is the only place the mapping can come from.
|
|
10695
10832
|
*/
|
|
10696
|
-
sendCollectionPatch(clientId, subscriptionId, id, row) {
|
|
10833
|
+
sendCollectionPatch(clientId, subscriptionId, id, row, notifyPath) {
|
|
10697
10834
|
const message = {
|
|
10698
10835
|
type: "collection_patch",
|
|
10699
10836
|
subscriptionId,
|
|
10700
10837
|
id,
|
|
10701
|
-
row
|
|
10838
|
+
row,
|
|
10839
|
+
pks: this.primaryKeysForPath(notifyPath)
|
|
10702
10840
|
};
|
|
10703
10841
|
this.sendMessage(clientId, message);
|
|
10704
10842
|
}
|
|
10843
|
+
/** The key columns of the collection at `path`, if they can be resolved. */
|
|
10844
|
+
primaryKeysForPath(path) {
|
|
10845
|
+
try {
|
|
10846
|
+
const collection = this.registry.getCollectionByPath(path);
|
|
10847
|
+
if (!collection) return void 0;
|
|
10848
|
+
const keys = getPrimaryKeys(collection, this.registry);
|
|
10849
|
+
return keys.length > 0 ? keys : void 0;
|
|
10850
|
+
} catch {
|
|
10851
|
+
return;
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10705
10854
|
sendError(clientId, error, subscriptionId, code) {
|
|
10706
10855
|
const message = {
|
|
10707
10856
|
type: "error",
|
|
@@ -10949,12 +11098,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10949
11098
|
}
|
|
10950
11099
|
/** Compute the canonical (possibly composite) id string from a captured row. */
|
|
10951
11100
|
extractIdFromCdcRow(collection, row) {
|
|
10952
|
-
|
|
10953
|
-
const composite = buildCompositeId(row, getPrimaryKeys(collection, this.registry));
|
|
10954
|
-
if (composite && composite !== ":::") return composite;
|
|
10955
|
-
} catch {}
|
|
10956
|
-
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
10957
|
-
return "*";
|
|
11101
|
+
return deriveRowAddress(row, collection, this.registry) || "*";
|
|
10958
11102
|
}
|
|
10959
11103
|
dedupKey(path, id, databaseId) {
|
|
10960
11104
|
return `${databaseId ?? ""}::${path}::${id}`;
|
|
@@ -20414,6 +20558,33 @@ function formatBytes(bytes) {
|
|
|
20414
20558
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
20415
20559
|
}
|
|
20416
20560
|
//#endregion
|
|
20561
|
+
//#region src/collections/buildRegistry.ts
|
|
20562
|
+
/**
|
|
20563
|
+
* Build the collection registry for a driver.
|
|
20564
|
+
*
|
|
20565
|
+
* The order matters and is the reason this is one function rather than a run of
|
|
20566
|
+
* statements in the bootstrapper. Keys are resolved from the drizzle schema, so
|
|
20567
|
+
* anything that inspects them has to run *after* the tables are registered —
|
|
20568
|
+
* and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
|
|
20569
|
+
* collection whose table it cannot look up is one it has nothing to say about.
|
|
20570
|
+
* Warned too early, it would skip every collection and report nothing, which
|
|
20571
|
+
* reads exactly like having nothing to report.
|
|
20572
|
+
*/
|
|
20573
|
+
function buildCollectionRegistry(schema) {
|
|
20574
|
+
const registry = new PostgresCollectionRegistry();
|
|
20575
|
+
if (schema.collections) {
|
|
20576
|
+
registry.registerMultiple(schema.collections);
|
|
20577
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
20578
|
+
}
|
|
20579
|
+
if (schema.tables) Object.values(schema.tables).forEach((table) => {
|
|
20580
|
+
if (isTable(table)) registry.registerTable(table, getTableName(table));
|
|
20581
|
+
});
|
|
20582
|
+
if (schema.enums) registry.registerEnums(schema.enums);
|
|
20583
|
+
if (schema.relations) registry.registerRelations(schema.relations);
|
|
20584
|
+
warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
|
|
20585
|
+
return registry;
|
|
20586
|
+
}
|
|
20587
|
+
//#endregion
|
|
20417
20588
|
//#region src/auth/ensure-tables.ts
|
|
20418
20589
|
/**
|
|
20419
20590
|
* Auto-create auth tables if they don't exist.
|
|
@@ -22396,21 +22567,14 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22396
22567
|
logger.info(`🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema "${pgSchemaName}" [${introspectedCollections.map((c) => c.slug).join(", ")}]`);
|
|
22397
22568
|
}
|
|
22398
22569
|
const activeCollections = introspectedCollections ?? collections;
|
|
22399
|
-
const registry = new PostgresCollectionRegistry();
|
|
22400
|
-
if (activeCollections) {
|
|
22401
|
-
registry.registerMultiple(activeCollections);
|
|
22402
|
-
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
22403
|
-
}
|
|
22404
22570
|
const schemaTables = introspectedTables ?? pgConfig.schema?.tables;
|
|
22405
|
-
if (schemaTables) Object.values(schemaTables).forEach((table) => {
|
|
22406
|
-
if (isTable(table)) {
|
|
22407
|
-
const tableName = getTableName(table);
|
|
22408
|
-
registry.registerTable(table, tableName);
|
|
22409
|
-
}
|
|
22410
|
-
});
|
|
22411
|
-
if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums);
|
|
22412
22571
|
const schemaRelations = introspectedRelations ?? pgConfig.schema?.relations;
|
|
22413
|
-
|
|
22572
|
+
const registry = buildCollectionRegistry({
|
|
22573
|
+
collections: activeCollections,
|
|
22574
|
+
tables: schemaTables,
|
|
22575
|
+
enums: pgConfig.schema?.enums,
|
|
22576
|
+
relations: schemaRelations
|
|
22577
|
+
});
|
|
22414
22578
|
if (schemaTables) patchPgArrayNullSafety(schemaTables);
|
|
22415
22579
|
const mergedSchema = {
|
|
22416
22580
|
...schemaTables,
|
|
@@ -22489,6 +22653,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22489
22653
|
const wantsCdc = cdcMode !== "off";
|
|
22490
22654
|
const explicitCdc = cdcMode === "trigger" || cdcMode === "wal";
|
|
22491
22655
|
let cdcEnabled = false;
|
|
22656
|
+
let provisionCdcForTables;
|
|
22492
22657
|
if (wantsCdc && !directUrl) {
|
|
22493
22658
|
const reason = "no direct database connection is available for the realtime LISTEN client (set DATABASE_DIRECT_URL)";
|
|
22494
22659
|
if (explicitCdc) logger.warn(`⚠️ [CDC] REALTIME_CDC=${cdcMode} but ${reason} — using app-level realtime.`);
|
|
@@ -22505,6 +22670,9 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22505
22670
|
})).filter((t) => Boolean(t.table) && registry.hasTableForCollection(t.table)));
|
|
22506
22671
|
await realtimeService.enableCdc(directUrl);
|
|
22507
22672
|
cdcEnabled = true;
|
|
22673
|
+
provisionCdcForTables = async (tables) => {
|
|
22674
|
+
await provisionTriggerCdc(cdcRunSql, tables);
|
|
22675
|
+
};
|
|
22508
22676
|
logger.info(`📡 [CDC] Realtime source = database-level change capture (mode: ${cdcMode === "wal" ? "wal→trigger" : "trigger"}). All writes now emit realtime events regardless of origin.`);
|
|
22509
22677
|
} catch (err) {
|
|
22510
22678
|
if (explicitCdc) logger.warn("⚠️ [CDC] Could not enable database-level change capture — falling back to app-level realtime.", { error: err });
|
|
@@ -22529,13 +22697,14 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22529
22697
|
const dbTables = new Set(result.rows.map((r) => r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`));
|
|
22530
22698
|
const missing = [];
|
|
22531
22699
|
for (const col of registeredCollections) {
|
|
22700
|
+
if (col.auth?.enabled) continue;
|
|
22532
22701
|
const schemaName = "schema" in col && col.schema ? col.schema : "public";
|
|
22533
22702
|
const tableName = registry.hasTableForCollection(col.table ?? col.slug) ? col.table ?? col.slug : col.slug;
|
|
22534
22703
|
const checkName = registry.getTableNames().find((k) => k === tableName || k === col.slug) ?? tableName;
|
|
22535
22704
|
const fullCheckName = schemaName === "public" ? checkName : `${schemaName}.${checkName}`;
|
|
22536
22705
|
if (!dbTables.has(fullCheckName)) missing.push({
|
|
22537
22706
|
slug: col.slug,
|
|
22538
|
-
table:
|
|
22707
|
+
table: fullCheckName
|
|
22539
22708
|
});
|
|
22540
22709
|
}
|
|
22541
22710
|
if (missing.length > 0) {
|
|
@@ -22569,7 +22738,8 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22569
22738
|
registry,
|
|
22570
22739
|
realtimeService,
|
|
22571
22740
|
driver,
|
|
22572
|
-
poolManager
|
|
22741
|
+
poolManager,
|
|
22742
|
+
provisionCdcForTables
|
|
22573
22743
|
}
|
|
22574
22744
|
};
|
|
22575
22745
|
},
|
|
@@ -22581,6 +22751,18 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22581
22751
|
const registry = internals.registry;
|
|
22582
22752
|
const authCollection = authConfig.collection;
|
|
22583
22753
|
await ensureAuthTablesExist(db, authCollection);
|
|
22754
|
+
if (authCollection && internals.provisionCdcForTables) {
|
|
22755
|
+
const authSchema = "schema" in authCollection && typeof authCollection.schema === "string" ? authCollection.schema : "rebase";
|
|
22756
|
+
const authTable = "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug;
|
|
22757
|
+
if (authTable) try {
|
|
22758
|
+
await internals.provisionCdcForTables([{
|
|
22759
|
+
schema: authSchema,
|
|
22760
|
+
table: authTable
|
|
22761
|
+
}]);
|
|
22762
|
+
} catch (err) {
|
|
22763
|
+
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) });
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22584
22766
|
let emailService;
|
|
22585
22767
|
if (authConfig.email) emailService = createEmailService(authConfig.email);
|
|
22586
22768
|
const tableName = authCollection ? "table" in authCollection && typeof authCollection.table === "string" ? authCollection.table : authCollection.slug : void 0;
|