@rebasepro/server-postgres 0.9.1-canary.7dddf96 → 0.9.1-canary.a57c262
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/index.es.js +253 -212
- package/dist/index.es.js.map +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 +60 -0
- package/package.json +10 -7
- package/src/schema/introspect-db.ts +8 -1
- package/src/services/FetchService.ts +29 -163
- 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 +176 -0
package/dist/index.es.js
CHANGED
|
@@ -4549,8 +4549,26 @@ function getTableForCollection(collection, registry) {
|
|
|
4549
4549
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4550
4550
|
return table;
|
|
4551
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
|
+
*/
|
|
4552
4571
|
function getPrimaryKeys(collection, registry) {
|
|
4553
|
-
const table = getTableForCollection(collection, registry);
|
|
4554
4572
|
if (collection.properties) {
|
|
4555
4573
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4556
4574
|
fieldName: key,
|
|
@@ -4559,6 +4577,8 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4559
4577
|
}));
|
|
4560
4578
|
if (idProps.length > 0) return idProps;
|
|
4561
4579
|
}
|
|
4580
|
+
const table = registry.getTable(getTableName$1(collection));
|
|
4581
|
+
if (!table) return [];
|
|
4562
4582
|
const keys = [];
|
|
4563
4583
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4564
4584
|
const col = colRaw;
|
|
@@ -4587,6 +4607,21 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4587
4607
|
return keys;
|
|
4588
4608
|
}
|
|
4589
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
|
+
/**
|
|
4590
4625
|
* Collections whose key the *browser* cannot resolve, and what it will do
|
|
4591
4626
|
* instead.
|
|
4592
4627
|
*
|
|
@@ -4613,12 +4648,7 @@ function findUnresolvableKeyCollections(collections, registry) {
|
|
|
4613
4648
|
const findings = [];
|
|
4614
4649
|
for (const collection of collections) {
|
|
4615
4650
|
if (getDeclaredPrimaryKeys(collection).length > 0) continue;
|
|
4616
|
-
|
|
4617
|
-
try {
|
|
4618
|
-
keys = getPrimaryKeys(collection, registry);
|
|
4619
|
-
} catch {
|
|
4620
|
-
continue;
|
|
4621
|
-
}
|
|
4651
|
+
const keys = getPrimaryKeys(collection, registry);
|
|
4622
4652
|
if (keys.length === 0) continue;
|
|
4623
4653
|
if (keys.length === 1 && keys[0].fieldName === "id") continue;
|
|
4624
4654
|
findings.push({
|
|
@@ -4649,19 +4679,14 @@ function warnOnKeysTheAdminCannotResolve(collections, registry) {
|
|
|
4649
4679
|
* The address of a row: derived from the collection's primary keys, because a
|
|
4650
4680
|
* row does not carry one — it is exactly its columns.
|
|
4651
4681
|
*
|
|
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).
|
|
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).
|
|
4659
4686
|
*/
|
|
4660
4687
|
function deriveRowAddress(row, collection, registry) {
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4664
|
-
} catch {}
|
|
4688
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, registry));
|
|
4689
|
+
if (composite && composite.split(":::").some((part) => part !== "")) return composite;
|
|
4665
4690
|
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
4666
4691
|
return "";
|
|
4667
4692
|
}
|
|
@@ -5703,6 +5728,63 @@ var RelationService = class {
|
|
|
5703
5728
|
this.registry = registry;
|
|
5704
5729
|
}
|
|
5705
5730
|
/**
|
|
5731
|
+
* One target row, as the {@link RelatedRow} everything here returns.
|
|
5732
|
+
*
|
|
5733
|
+
* Eight sites built this by hand, which is how the address came to be the
|
|
5734
|
+
* target's first key column in all eight — one edit, eight places to miss.
|
|
5735
|
+
*
|
|
5736
|
+
* `resolveNested` is the one thing they did not agree on, and the
|
|
5737
|
+
* disagreement was invisible: the single-parent fetches pass `db` and
|
|
5738
|
+
* `registry` to `parseDataFromServer`, so the target's *own* relations get
|
|
5739
|
+
* resolved too, while the batch paths deliberately do not — a query per
|
|
5740
|
+
* target row is the N+1 the batching exists to avoid. Naming the parameter
|
|
5741
|
+
* makes that a decision rather than a difference between two call sites
|
|
5742
|
+
* nobody was comparing.
|
|
5743
|
+
*/
|
|
5744
|
+
async toRelatedRow(targetRow, targetCollection, targetPks, options) {
|
|
5745
|
+
const values = options?.resolveNested ? await parseDataFromServer(targetRow, targetCollection, this.db, this.registry) : await parseDataFromServer(targetRow, targetCollection);
|
|
5746
|
+
return {
|
|
5747
|
+
id: buildCompositeId(targetRow, targetPks),
|
|
5748
|
+
path: targetCollection.slug,
|
|
5749
|
+
values
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
/**
|
|
5753
|
+
* A WHERE matching any of `parentIds`, by the whole key.
|
|
5754
|
+
*
|
|
5755
|
+
* A single key is an `IN (…)`. A composite one cannot be: matching
|
|
5756
|
+
* `tenant_id IN (1, 1)` collects every row of tenant 1, so two parents that
|
|
5757
|
+
* share their first column each receive the other's relations. It becomes
|
|
5758
|
+
* an OR of ANDs — one exact address per parent — which Postgres indexes the
|
|
5759
|
+
* same way it would a multi-column key lookup.
|
|
5760
|
+
*/
|
|
5761
|
+
parentKeyCondition(parentTable, parentPks, parentIds) {
|
|
5762
|
+
const columnFor = (fieldName) => {
|
|
5763
|
+
const col = parentTable[fieldName];
|
|
5764
|
+
if (!col) throw new Error(`Key column '${fieldName}' not found in parent table`);
|
|
5765
|
+
return col;
|
|
5766
|
+
};
|
|
5767
|
+
if (parentPks.length === 1) {
|
|
5768
|
+
const values = parentIds.map((id) => parseIdValues(id, parentPks)[parentPks[0].fieldName]);
|
|
5769
|
+
return inArray(columnFor(parentPks[0].fieldName), values);
|
|
5770
|
+
}
|
|
5771
|
+
return or(...parentIds.map((id) => {
|
|
5772
|
+
const values = parseIdValues(id, parentPks);
|
|
5773
|
+
return and(...parentPks.map((pk) => eq(columnFor(pk.fieldName), values[pk.fieldName])));
|
|
5774
|
+
}));
|
|
5775
|
+
}
|
|
5776
|
+
/**
|
|
5777
|
+
* Reject a relation that cannot express a composite-keyed parent.
|
|
5778
|
+
*
|
|
5779
|
+
* `localKey` and `foreignKeyOnTarget` are single column names: one column
|
|
5780
|
+
* cannot reference a two-column key, so such a relation has no correct
|
|
5781
|
+
* reading. Left alone it would silently match on the first key column and
|
|
5782
|
+
* hand a tenant's rows to its neighbour — say so instead.
|
|
5783
|
+
*/
|
|
5784
|
+
assertSingleKeyAddressable(parentCollection, parentPks, via) {
|
|
5785
|
+
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.`);
|
|
5786
|
+
}
|
|
5787
|
+
/**
|
|
5706
5788
|
* Fetch rows related to a parent row through a specific relation
|
|
5707
5789
|
*/
|
|
5708
5790
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5721,9 +5803,9 @@ var RelationService = class {
|
|
|
5721
5803
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5722
5804
|
const targetCollection = relation.target();
|
|
5723
5805
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5724
|
-
const idInfo =
|
|
5806
|
+
const idInfo = requirePrimaryKeys(targetCollection, this.registry);
|
|
5725
5807
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5726
|
-
const parentPks =
|
|
5808
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5727
5809
|
const parentIdInfo = parentPks[0];
|
|
5728
5810
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5729
5811
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5747,7 +5829,7 @@ var RelationService = class {
|
|
|
5747
5829
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5748
5830
|
currentTable = joinTable;
|
|
5749
5831
|
}
|
|
5750
|
-
const parentIdField = parentTable[
|
|
5832
|
+
const parentIdField = parentTable[requirePrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5751
5833
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5752
5834
|
if (options.limit) query = query.limit(options.limit);
|
|
5753
5835
|
const results = await query;
|
|
@@ -5755,12 +5837,7 @@ var RelationService = class {
|
|
|
5755
5837
|
const rows = [];
|
|
5756
5838
|
for (const row of results) {
|
|
5757
5839
|
const targetRow = row[targetTableName] || row;
|
|
5758
|
-
|
|
5759
|
-
rows.push({
|
|
5760
|
-
id: buildCompositeId(targetRow, idInfo),
|
|
5761
|
-
path: targetCollection.slug,
|
|
5762
|
-
values: parsedValues
|
|
5763
|
-
});
|
|
5840
|
+
rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
|
|
5764
5841
|
}
|
|
5765
5842
|
return rows;
|
|
5766
5843
|
}
|
|
@@ -5779,12 +5856,7 @@ var RelationService = class {
|
|
|
5779
5856
|
const rows = [];
|
|
5780
5857
|
for (const row of results) {
|
|
5781
5858
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5782
|
-
|
|
5783
|
-
rows.push({
|
|
5784
|
-
id: buildCompositeId(targetRow, idInfo),
|
|
5785
|
-
path: targetCollection.slug,
|
|
5786
|
-
values: parsedValues
|
|
5787
|
-
});
|
|
5859
|
+
rows.push(await this.toRelatedRow(targetRow, targetCollection, idInfo, { resolveNested: true }));
|
|
5788
5860
|
}
|
|
5789
5861
|
return rows;
|
|
5790
5862
|
}
|
|
@@ -5801,8 +5873,8 @@ var RelationService = class {
|
|
|
5801
5873
|
}
|
|
5802
5874
|
const targetCollection = relation.target();
|
|
5803
5875
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5804
|
-
const targetIdField = targetTable[
|
|
5805
|
-
const parentPks =
|
|
5876
|
+
const targetIdField = targetTable[requirePrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5877
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5806
5878
|
const parentIdInfo = parentPks[0];
|
|
5807
5879
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5808
5880
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5821,10 +5893,10 @@ var RelationService = class {
|
|
|
5821
5893
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5822
5894
|
const targetCollection = relation.target();
|
|
5823
5895
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5824
|
-
const targetPks =
|
|
5896
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
5825
5897
|
const targetIdInfo = targetPks[0];
|
|
5826
5898
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5827
|
-
const parentPks =
|
|
5899
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5828
5900
|
const parentIdInfo = parentPks[0];
|
|
5829
5901
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5830
5902
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5848,25 +5920,19 @@ var RelationService = class {
|
|
|
5848
5920
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5849
5921
|
currentTable = joinTable;
|
|
5850
5922
|
}
|
|
5851
|
-
|
|
5852
|
-
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5923
|
+
query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
5853
5924
|
const results = await query;
|
|
5854
5925
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5855
5926
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5856
5927
|
for (const row of results) {
|
|
5857
5928
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5858
5929
|
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
|
-
});
|
|
5930
|
+
resultMap.set(buildCompositeId(parentRow, parentPks), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5866
5931
|
}
|
|
5867
5932
|
return resultMap;
|
|
5868
5933
|
}
|
|
5869
5934
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5935
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5870
5936
|
const localKeyCol = parentTable[relation.localKey];
|
|
5871
5937
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5872
5938
|
const fkRows = await this.db.select({
|
|
@@ -5895,17 +5961,11 @@ var RelationService = class {
|
|
|
5895
5961
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5896
5962
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
5897
5963
|
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
|
-
}
|
|
5964
|
+
if (targetRow) resultMap.set(parentIdStr, await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5906
5965
|
}
|
|
5907
5966
|
return resultMap;
|
|
5908
5967
|
}
|
|
5968
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
5909
5969
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
5910
5970
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5911
5971
|
const results = await query;
|
|
@@ -5916,14 +5976,7 @@ var RelationService = class {
|
|
|
5916
5976
|
let parentId;
|
|
5917
5977
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
5918
5978
|
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
|
-
}
|
|
5979
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) resultMap.set(String(parentId), await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5927
5980
|
}
|
|
5928
5981
|
return resultMap;
|
|
5929
5982
|
}
|
|
@@ -5937,9 +5990,9 @@ var RelationService = class {
|
|
|
5937
5990
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5938
5991
|
const targetCollection = relation.target();
|
|
5939
5992
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5940
|
-
const targetPks =
|
|
5993
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
5941
5994
|
const targetIdField = targetTable[targetPks[0].fieldName];
|
|
5942
|
-
const parentPks =
|
|
5995
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
5943
5996
|
const parentIdInfo = parentPks[0];
|
|
5944
5997
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5945
5998
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5961,27 +6014,22 @@ var RelationService = class {
|
|
|
5961
6014
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5962
6015
|
currentTable = joinTable;
|
|
5963
6016
|
}
|
|
5964
|
-
|
|
5965
|
-
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6017
|
+
query = query.where(this.parentKeyCondition(parentTable, parentPks, parentIds));
|
|
5966
6018
|
const results = await query;
|
|
5967
6019
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5968
6020
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5969
6021
|
for (const row of results) {
|
|
5970
6022
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5971
6023
|
const targetRow = row[targetTableName] || row;
|
|
5972
|
-
const parentId =
|
|
5973
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6024
|
+
const parentId = buildCompositeId(parentRow, parentPks);
|
|
5974
6025
|
const arr = resultMap.get(parentId) || [];
|
|
5975
|
-
arr.push(
|
|
5976
|
-
id: buildCompositeId(targetRow, targetPks),
|
|
5977
|
-
path: targetCollection.slug,
|
|
5978
|
-
values: parsedValues
|
|
5979
|
-
});
|
|
6026
|
+
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
5980
6027
|
resultMap.set(parentId, arr);
|
|
5981
6028
|
}
|
|
5982
6029
|
return resultMap;
|
|
5983
6030
|
}
|
|
5984
6031
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6032
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
5985
6033
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
5986
6034
|
if (!junctionTable) {
|
|
5987
6035
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -6000,17 +6048,13 @@ var RelationService = class {
|
|
|
6000
6048
|
const junctionData = row[relation.through.table] || row;
|
|
6001
6049
|
const targetData = row[targetTableName] || row;
|
|
6002
6050
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
6003
|
-
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
6004
6051
|
const arr = resultMap.get(parentId) || [];
|
|
6005
|
-
arr.push(
|
|
6006
|
-
id: buildCompositeId(targetData, targetPks),
|
|
6007
|
-
path: targetCollection.slug,
|
|
6008
|
-
values: parsedValues
|
|
6009
|
-
});
|
|
6052
|
+
arr.push(await this.toRelatedRow(targetData, targetCollection, targetPks));
|
|
6010
6053
|
resultMap.set(parentId, arr);
|
|
6011
6054
|
}
|
|
6012
6055
|
return resultMap;
|
|
6013
6056
|
}
|
|
6057
|
+
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6014
6058
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6015
6059
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6016
6060
|
const results = await query;
|
|
@@ -6023,14 +6067,9 @@ var RelationService = class {
|
|
|
6023
6067
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6024
6068
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6025
6069
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
6026
|
-
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6027
6070
|
const key = String(parentId);
|
|
6028
6071
|
const arr = resultMap.get(key) || [];
|
|
6029
|
-
arr.push(
|
|
6030
|
-
id: buildCompositeId(targetRow, targetPks),
|
|
6031
|
-
path: targetCollection.slug,
|
|
6032
|
-
values: parsedValues
|
|
6033
|
-
});
|
|
6072
|
+
arr.push(await this.toRelatedRow(targetRow, targetCollection, targetPks));
|
|
6034
6073
|
resultMap.set(key, arr);
|
|
6035
6074
|
}
|
|
6036
6075
|
}
|
|
@@ -6078,12 +6117,12 @@ var RelationService = class {
|
|
|
6078
6117
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
6079
6118
|
continue;
|
|
6080
6119
|
}
|
|
6081
|
-
const parentPks =
|
|
6120
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6082
6121
|
const parentIdInfo = parentPks[0];
|
|
6083
6122
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6084
6123
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6085
6124
|
if (targetEntityIds.length > 0) {
|
|
6086
|
-
const targetPks =
|
|
6125
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6087
6126
|
const targetIdInfo = targetPks[0];
|
|
6088
6127
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6089
6128
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6103,12 +6142,12 @@ var RelationService = class {
|
|
|
6103
6142
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6104
6143
|
continue;
|
|
6105
6144
|
}
|
|
6106
|
-
const parentPks =
|
|
6145
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6107
6146
|
const parentIdInfo = parentPks[0];
|
|
6108
6147
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6109
6148
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6110
6149
|
if (targetEntityIds.length > 0) {
|
|
6111
|
-
const targetPks =
|
|
6150
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6112
6151
|
const targetIdInfo = targetPks[0];
|
|
6113
6152
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6114
6153
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6119,7 +6158,7 @@ var RelationService = class {
|
|
|
6119
6158
|
} 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
6159
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6121
6160
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6122
|
-
const targetPks =
|
|
6161
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6123
6162
|
const targetIdInfo = targetPks[0];
|
|
6124
6163
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6125
6164
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6127,7 +6166,7 @@ var RelationService = class {
|
|
|
6127
6166
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6128
6167
|
continue;
|
|
6129
6168
|
}
|
|
6130
|
-
const parentPks =
|
|
6169
|
+
const parentPks = requirePrimaryKeys(collection, this.registry);
|
|
6131
6170
|
const parentIdInfo = parentPks[0];
|
|
6132
6171
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6133
6172
|
if (targetEntityIds.length > 0) {
|
|
@@ -6147,9 +6186,9 @@ var RelationService = class {
|
|
|
6147
6186
|
try {
|
|
6148
6187
|
const targetCollection = relation.target();
|
|
6149
6188
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6150
|
-
const targetPks =
|
|
6189
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6151
6190
|
const targetIdInfo = targetPks[0];
|
|
6152
|
-
const sourcePks =
|
|
6191
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6153
6192
|
const sourceIdInfo = sourcePks[0];
|
|
6154
6193
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6155
6194
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6230,12 +6269,12 @@ var RelationService = class {
|
|
|
6230
6269
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6231
6270
|
return;
|
|
6232
6271
|
}
|
|
6233
|
-
const sourcePks =
|
|
6272
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6234
6273
|
const sourceIdInfo = sourcePks[0];
|
|
6235
6274
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6236
6275
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6237
6276
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6238
|
-
const targetPks =
|
|
6277
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6239
6278
|
const targetIdInfo = targetPks[0];
|
|
6240
6279
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6241
6280
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6243,7 +6282,7 @@ var RelationService = class {
|
|
|
6243
6282
|
}));
|
|
6244
6283
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6245
6284
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6246
|
-
const targetPks =
|
|
6285
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6247
6286
|
const targetIdInfo = targetPks[0];
|
|
6248
6287
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6249
6288
|
const newLink = {
|
|
@@ -6274,12 +6313,12 @@ var RelationService = class {
|
|
|
6274
6313
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6275
6314
|
return;
|
|
6276
6315
|
}
|
|
6277
|
-
const sourcePks =
|
|
6316
|
+
const sourcePks = requirePrimaryKeys(sourceCollection, this.registry);
|
|
6278
6317
|
const sourceIdInfo = sourcePks[0];
|
|
6279
6318
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6280
6319
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6281
6320
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6282
|
-
const targetPks =
|
|
6321
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6283
6322
|
const targetIdInfo = targetPks[0];
|
|
6284
6323
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6285
6324
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6300,12 +6339,12 @@ var RelationService = class {
|
|
|
6300
6339
|
const { relation, newTargetId } = upd;
|
|
6301
6340
|
const targetCollection = relation.target();
|
|
6302
6341
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6303
|
-
const targetPks =
|
|
6342
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6304
6343
|
const targetIdInfo = targetPks[0];
|
|
6305
6344
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6306
6345
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6307
6346
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6308
|
-
const parentPks =
|
|
6347
|
+
const parentPks = requirePrimaryKeys(parentCollection, this.registry);
|
|
6309
6348
|
const parentIdInfo = parentPks[0];
|
|
6310
6349
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6311
6350
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6376,7 +6415,7 @@ var RelationService = class {
|
|
|
6376
6415
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6377
6416
|
return;
|
|
6378
6417
|
}
|
|
6379
|
-
const targetPks =
|
|
6418
|
+
const targetPks = requirePrimaryKeys(targetCollection, this.registry);
|
|
6380
6419
|
const targetIdInfo = targetPks[0];
|
|
6381
6420
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6382
6421
|
const junctionData = {
|
|
@@ -6392,6 +6431,100 @@ var RelationService = class {
|
|
|
6392
6431
|
}
|
|
6393
6432
|
};
|
|
6394
6433
|
//#endregion
|
|
6434
|
+
//#region src/services/row-pipeline.ts
|
|
6435
|
+
/**
|
|
6436
|
+
* Whether a many-relation reaches its target through a junction table.
|
|
6437
|
+
*
|
|
6438
|
+
* Also used to build the drizzle `with` config, which is why it is exported:
|
|
6439
|
+
* the query has to nest one level deeper for a junction, and the row walk has
|
|
6440
|
+
* to unwrap that same level back out.
|
|
6441
|
+
*/
|
|
6442
|
+
function isJunctionRelation(relation) {
|
|
6443
|
+
if (relation.through) return true;
|
|
6444
|
+
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6445
|
+
return false;
|
|
6446
|
+
}
|
|
6447
|
+
/**
|
|
6448
|
+
* Reach the target row inside a junction row.
|
|
6449
|
+
*
|
|
6450
|
+
* A junction row looks like `{ post_id: 1, tag_id: { id: 5, name: "ts" } }` —
|
|
6451
|
+
* the foreign keys, and the target nested under one of them. The target is the
|
|
6452
|
+
* only object among them, so that is how it is found. A junction row that has
|
|
6453
|
+
* not been nested (no `with` on the join) has no object and is returned as-is.
|
|
6454
|
+
*/
|
|
6455
|
+
function unwrapJunctionRow(item) {
|
|
6456
|
+
const nestedKey = Object.keys(item).find((key) => typeof item[key] === "object" && item[key] !== null && !Array.isArray(item[key]));
|
|
6457
|
+
return nestedKey ? item[nestedKey] : item;
|
|
6458
|
+
}
|
|
6459
|
+
/** Render one target row in the requested style. */
|
|
6460
|
+
function renderTarget(targetRow, targetCollection, style, registry) {
|
|
6461
|
+
if (style === "inline") return { ...targetRow };
|
|
6462
|
+
const address = relationTargetAddress(targetRow, targetCollection, registry);
|
|
6463
|
+
const path = targetCollection.slug;
|
|
6464
|
+
return createRelationRefWithData(address, path, {
|
|
6465
|
+
id: address,
|
|
6466
|
+
path,
|
|
6467
|
+
values: normalizeDbValues(targetRow, targetCollection)
|
|
6468
|
+
});
|
|
6469
|
+
}
|
|
6470
|
+
/**
|
|
6471
|
+
* The address a relation ref points at.
|
|
6472
|
+
*
|
|
6473
|
+
* The whole key, not its first column: a composite-keyed target addressed by
|
|
6474
|
+
* `tenant_id` alone points at every row that shares it. A target whose key
|
|
6475
|
+
* cannot be resolved at all used to throw here — reading `[0]` of an empty
|
|
6476
|
+
* array — taking down the parent's fetch over a relation it may not even have
|
|
6477
|
+
* asked for. The first column is a guess, but a ref that resolves to nothing
|
|
6478
|
+
* beats no rows at all.
|
|
6479
|
+
*/
|
|
6480
|
+
function relationTargetAddress(targetRow, targetCollection, registry) {
|
|
6481
|
+
const address = deriveRowAddress(targetRow, targetCollection, registry);
|
|
6482
|
+
if (address) return address;
|
|
6483
|
+
return String(targetRow[Object.keys(targetRow)[0]] ?? "");
|
|
6484
|
+
}
|
|
6485
|
+
/**
|
|
6486
|
+
* The row the admin renders: every column, with relations as references.
|
|
6487
|
+
*
|
|
6488
|
+
* Values are normalized (dates, numbers, NaN) because the admin's view-model
|
|
6489
|
+
* expects real types. The row's own address is *not* among the columns — it is
|
|
6490
|
+
* derived by the consumer from the collection's primary keys.
|
|
6491
|
+
*/
|
|
6492
|
+
function toCmsRow(row, collection, registry) {
|
|
6493
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6494
|
+
const normalized = normalizeDbValues(row, collection);
|
|
6495
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6496
|
+
const relData = row[relation.relationName || key];
|
|
6497
|
+
if (relData === void 0 || relData === null) continue;
|
|
6498
|
+
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6499
|
+
const targetCollection = relation.target();
|
|
6500
|
+
normalized[key] = relData.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, targetCollection, "ref", registry));
|
|
6501
|
+
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) normalized[key] = renderTarget(relData, relation.target(), "ref", registry);
|
|
6502
|
+
}
|
|
6503
|
+
return normalized;
|
|
6504
|
+
}
|
|
6505
|
+
/**
|
|
6506
|
+
* The row REST serves: every column under its own name, with the value Postgres
|
|
6507
|
+
* returned, and relations inlined as the target's columns.
|
|
6508
|
+
*
|
|
6509
|
+
* Deliberately not normalized: REST serves what the database holds, and JSON
|
|
6510
|
+
* has its own opinions about dates that the admin's view-model does not share.
|
|
6511
|
+
*
|
|
6512
|
+
* Keyed by the row rather than by the relation list — a REST fetch only loads
|
|
6513
|
+
* the relations `include` asked for, so the row is the authority on which are
|
|
6514
|
+
* actually there.
|
|
6515
|
+
*/
|
|
6516
|
+
function toRestRow(row, collection, registry) {
|
|
6517
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6518
|
+
const flat = {};
|
|
6519
|
+
for (const [key, value] of Object.entries(row)) {
|
|
6520
|
+
const relation = findRelation(resolvedRelations, key);
|
|
6521
|
+
if (relation && Array.isArray(value)) flat[key] = value.map((item) => renderTarget(isJunctionRelation(relation) ? unwrapJunctionRow(item) : item, relation.target(), "inline", registry));
|
|
6522
|
+
else if (relation && typeof value === "object" && value !== null) flat[key] = renderTarget(value, relation.target(), "inline", registry);
|
|
6523
|
+
else flat[key] = value;
|
|
6524
|
+
}
|
|
6525
|
+
return flat;
|
|
6526
|
+
}
|
|
6527
|
+
//#endregion
|
|
6395
6528
|
//#region src/services/FetchService.ts
|
|
6396
6529
|
/**
|
|
6397
6530
|
* Service for handling all row read operations.
|
|
@@ -6450,7 +6583,7 @@ var FetchService = class {
|
|
|
6450
6583
|
if (!shouldInclude(key)) continue;
|
|
6451
6584
|
const drizzleRelName = relation.relationName || key;
|
|
6452
6585
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6453
|
-
if (relation.cardinality === "many" &&
|
|
6586
|
+
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
6454
6587
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6455
6588
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6456
6589
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6459,14 +6592,6 @@ var FetchService = class {
|
|
|
6459
6592
|
return withConfig;
|
|
6460
6593
|
}
|
|
6461
6594
|
/**
|
|
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
6595
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6471
6596
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6472
6597
|
*/
|
|
@@ -6475,68 +6600,6 @@ var FetchService = class {
|
|
|
6475
6600
|
return null;
|
|
6476
6601
|
}
|
|
6477
6602
|
/**
|
|
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
6603
|
* Post-fetch joinPath relations for a single flat row.
|
|
6541
6604
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6542
6605
|
* so they must be loaded separately after the primary query.
|
|
@@ -6567,8 +6630,10 @@ var FetchService = class {
|
|
|
6567
6630
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6568
6631
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6569
6632
|
if (joinPathRelations.length === 0) return;
|
|
6570
|
-
const
|
|
6571
|
-
|
|
6633
|
+
const parentIdOf = (row) => {
|
|
6634
|
+
const address = buildCompositeId(row, idInfoArray);
|
|
6635
|
+
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6636
|
+
};
|
|
6572
6637
|
for (const [key, relation] of joinPathRelations) try {
|
|
6573
6638
|
const addressable = rows.filter((r) => parentIdOf(r) !== void 0 && parentIdOf(r) !== null);
|
|
6574
6639
|
if (addressable.length === 0) continue;
|
|
@@ -6588,32 +6653,6 @@ var FetchService = class {
|
|
|
6588
6653
|
}
|
|
6589
6654
|
}
|
|
6590
6655
|
/**
|
|
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
6656
|
* Build db.query-compatible options from standard fetch options.
|
|
6618
6657
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6619
6658
|
*/
|
|
@@ -6683,7 +6722,7 @@ var FetchService = class {
|
|
|
6683
6722
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6684
6723
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6685
6724
|
const table = getTableForCollection(collection, this.registry);
|
|
6686
|
-
const idInfoArray =
|
|
6725
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6687
6726
|
const idInfo = idInfoArray[0];
|
|
6688
6727
|
const idField = table[idInfo.fieldName];
|
|
6689
6728
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6697,7 +6736,7 @@ var FetchService = class {
|
|
|
6697
6736
|
with: withConfig
|
|
6698
6737
|
});
|
|
6699
6738
|
if (!row) return void 0;
|
|
6700
|
-
const flatRow =
|
|
6739
|
+
const flatRow = toCmsRow(row, collection, this.registry);
|
|
6701
6740
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6702
6741
|
return flatRow;
|
|
6703
6742
|
} catch (e) {
|
|
@@ -6739,7 +6778,7 @@ var FetchService = class {
|
|
|
6739
6778
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6740
6779
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6741
6780
|
const table = getTableForCollection(collection, this.registry);
|
|
6742
|
-
const idInfoArray =
|
|
6781
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6743
6782
|
const idInfo = idInfoArray[0];
|
|
6744
6783
|
const idField = table[idInfo.fieldName];
|
|
6745
6784
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6749,7 +6788,7 @@ var FetchService = class {
|
|
|
6749
6788
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6750
6789
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6751
6790
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6752
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6791
|
+
return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
|
|
6753
6792
|
} catch (e) {
|
|
6754
6793
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6755
6794
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6810,8 +6849,10 @@ var FetchService = class {
|
|
|
6810
6849
|
}
|
|
6811
6850
|
/**
|
|
6812
6851
|
* Fallback path used when db.query is unavailable.
|
|
6813
|
-
*
|
|
6814
|
-
*
|
|
6852
|
+
*
|
|
6853
|
+
* The primary path runs the results through `toCmsRow`, which maps
|
|
6854
|
+
* relations from what drizzle already nested — no query per row. This one
|
|
6855
|
+
* has no nesting to read, so it resolves relations itself, in batches.
|
|
6815
6856
|
*
|
|
6816
6857
|
* Process raw database results into flat rows with relations.
|
|
6817
6858
|
*/
|
|
@@ -6953,7 +6994,7 @@ var FetchService = class {
|
|
|
6953
6994
|
if (value === void 0 || value === null) return true;
|
|
6954
6995
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6955
6996
|
const table = getTableForCollection(collection, this.registry);
|
|
6956
|
-
const idInfoArray =
|
|
6997
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6957
6998
|
const idInfo = idInfoArray[0];
|
|
6958
6999
|
const idField = table[idInfo.fieldName];
|
|
6959
7000
|
const field = table[fieldName];
|
|
@@ -6980,7 +7021,7 @@ var FetchService = class {
|
|
|
6980
7021
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
6981
7022
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6982
7023
|
const table = getTableForCollection(collection, this.registry);
|
|
6983
|
-
const idInfoArray =
|
|
7024
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
6984
7025
|
const idInfo = idInfoArray[0];
|
|
6985
7026
|
const idField = table[idInfo.fieldName];
|
|
6986
7027
|
const tableName = getTableName(table);
|
|
@@ -6988,7 +7029,7 @@ var FetchService = class {
|
|
|
6988
7029
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
6989
7030
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
6990
7031
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
6991
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
7032
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => toRestRow(row, collection, this.registry));
|
|
6992
7033
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
6993
7034
|
return restRows;
|
|
6994
7035
|
} catch (e) {
|
|
@@ -7037,7 +7078,7 @@ var FetchService = class {
|
|
|
7037
7078
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
7038
7079
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7039
7080
|
const table = getTableForCollection(collection, this.registry);
|
|
7040
|
-
const idInfoArray =
|
|
7081
|
+
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
7041
7082
|
const idInfo = idInfoArray[0];
|
|
7042
7083
|
const idField = table[idInfo.fieldName];
|
|
7043
7084
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -7050,7 +7091,7 @@ var FetchService = class {
|
|
|
7050
7091
|
...withConfig ? { with: withConfig } : {}
|
|
7051
7092
|
});
|
|
7052
7093
|
if (!row) return null;
|
|
7053
|
-
const restRow =
|
|
7094
|
+
const restRow = toRestRow(row, collection, this.registry);
|
|
7054
7095
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
7055
7096
|
return restRow;
|
|
7056
7097
|
} catch (e) {
|
|
@@ -7095,7 +7136,7 @@ var FetchService = class {
|
|
|
7095
7136
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7096
7137
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7097
7138
|
const table = getTableForCollection(collection, this.registry);
|
|
7098
|
-
const idField = table[
|
|
7139
|
+
const idField = table[requirePrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7099
7140
|
let vectorMeta;
|
|
7100
7141
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7101
7142
|
let query = vectorMeta ? this.db.select({
|