@rebasepro/server-postgres 0.9.1-canary.a57c262 → 0.9.1-canary.ad25bc0
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/data-transformer.d.ts +2 -9
- package/dist/index.es.js +298 -639
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +32 -4
- package/dist/services/RelationService.d.ts +1 -34
- package/dist/services/collection-helpers.d.ts +0 -76
- package/dist/services/index.d.ts +1 -1
- package/dist/services/realtimeService.d.ts +0 -7
- package/package.json +7 -10
- package/src/PostgresBackendDriver.ts +6 -21
- package/src/PostgresBootstrapper.ts +23 -11
- package/src/data-transformer.ts +9 -11
- package/src/schema/generate-drizzle-schema-logic.ts +4 -21
- package/src/schema/generate-postgres-ddl-logic.ts +3 -63
- package/src/schema/introspect-db.ts +1 -8
- package/src/services/FetchService.ts +229 -50
- package/src/services/RelationService.ts +94 -153
- package/src/services/collection-helpers.ts +3 -166
- package/src/services/index.ts +0 -1
- package/src/services/realtimeService.ts +19 -40
- package/dist/collections/buildRegistry.d.ts +0 -27
- package/dist/services/row-pipeline.d.ts +0 -60
- package/src/collections/buildRegistry.ts +0 -59
- package/src/services/row-pipeline.ts +0 -176
package/dist/index.es.js
CHANGED
|
@@ -1808,17 +1808,12 @@ 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
|
-
*
|
|
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.
|
|
1811
|
+
* The driver can also infer keys from the Drizzle schema, which the browser
|
|
1812
|
+
* cannot see — so the server normalizes what it resolved onto the config it
|
|
1813
|
+
* serves (see `stampPrimaryKeys`), and this reads that back. Returns an empty
|
|
1814
|
+
* array when a collection declares none, which callers must treat as "not
|
|
1815
|
+
* addressable" rather than defaulting to `id`: guessing a key that is not the
|
|
1816
|
+
* real one produces confidently wrong addresses.
|
|
1822
1817
|
*/
|
|
1823
1818
|
function getDeclaredPrimaryKeys(collection) {
|
|
1824
1819
|
const properties = collection.properties;
|
|
@@ -1843,15 +1838,9 @@ function getDeclaredPrimaryKeys(collection) {
|
|
|
1843
1838
|
* The postgres driver tries, in order: properties marked `isId`; the primary
|
|
1844
1839
|
* keys of the Drizzle schema; and finally a column literally named `id`. Only
|
|
1845
1840
|
* the first and last are visible in a `CollectionConfig`, which is what both
|
|
1846
|
-
* sides share
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
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.
|
|
1841
|
+
* sides share — so a collection whose key is *only* known to Drizzle, and is
|
|
1842
|
+
* not named `id`, resolves to nothing here. That is reported rather than
|
|
1843
|
+
* guessed: inventing a key produces addresses that look right and route wrong.
|
|
1855
1844
|
*/
|
|
1856
1845
|
function resolvePrimaryKeys(collection) {
|
|
1857
1846
|
const declared = getDeclaredPrimaryKeys(collection);
|
|
@@ -2563,7 +2552,7 @@ var buildPropertyCallbacks = (properties) => {
|
|
|
2563
2552
|
* Opt out with `disableDefaultPolicies: true` to take full responsibility for
|
|
2564
2553
|
* the collection's RLS.
|
|
2565
2554
|
*/
|
|
2566
|
-
var SERVER_OR_ADMIN_EXPR
|
|
2555
|
+
var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
2567
2556
|
/** Write operations that must be admin-gated by default on auth collections. */
|
|
2568
2557
|
var DEFAULT_GUARDED_OPS = [
|
|
2569
2558
|
"insert",
|
|
@@ -2576,7 +2565,7 @@ function isAuthCollection(collection) {
|
|
|
2576
2565
|
return auth === true || typeof auth === "object" && auth?.enabled === true;
|
|
2577
2566
|
}
|
|
2578
2567
|
/** The property marked as the row id (falls back to `id`). */
|
|
2579
|
-
function getIdPropertyName
|
|
2568
|
+
function getIdPropertyName(collection) {
|
|
2580
2569
|
for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
|
|
2581
2570
|
return "id";
|
|
2582
2571
|
}
|
|
@@ -2596,232 +2585,30 @@ function getEffectiveSecurityRules(collection) {
|
|
|
2596
2585
|
injected.push({
|
|
2597
2586
|
name: `${tableName}_default_admin_read`,
|
|
2598
2587
|
operations: ["select"],
|
|
2599
|
-
condition: SERVER_OR_ADMIN_EXPR
|
|
2588
|
+
condition: SERVER_OR_ADMIN_EXPR
|
|
2600
2589
|
});
|
|
2601
2590
|
injected.push({
|
|
2602
2591
|
name: `${tableName}_default_admin_write`,
|
|
2603
2592
|
operations: [...DEFAULT_GUARDED_OPS],
|
|
2604
|
-
condition: SERVER_OR_ADMIN_EXPR
|
|
2605
|
-
check: SERVER_OR_ADMIN_EXPR
|
|
2593
|
+
condition: SERVER_OR_ADMIN_EXPR,
|
|
2594
|
+
check: SERVER_OR_ADMIN_EXPR
|
|
2606
2595
|
});
|
|
2607
2596
|
if (isAuthCollection(collection)) {
|
|
2608
2597
|
injected.push({
|
|
2609
2598
|
name: `${tableName}_default_self_read`,
|
|
2610
2599
|
operations: ["select"],
|
|
2611
|
-
condition: policy.compare(policy.field(getIdPropertyName
|
|
2600
|
+
condition: policy.compare(policy.field(getIdPropertyName(collection)), "eq", policy.authUid())
|
|
2612
2601
|
});
|
|
2613
2602
|
injected.push({
|
|
2614
2603
|
name: `${tableName}_require_admin_write`,
|
|
2615
2604
|
mode: "restrictive",
|
|
2616
2605
|
operations: [...DEFAULT_GUARDED_OPS],
|
|
2617
|
-
condition: SERVER_OR_ADMIN_EXPR
|
|
2618
|
-
check: SERVER_OR_ADMIN_EXPR
|
|
2606
|
+
condition: SERVER_OR_ADMIN_EXPR,
|
|
2607
|
+
check: SERVER_OR_ADMIN_EXPR
|
|
2619
2608
|
});
|
|
2620
2609
|
}
|
|
2621
2610
|
return [...explicit, ...injected];
|
|
2622
2611
|
}
|
|
2623
|
-
//#endregion
|
|
2624
|
-
//#region ../common/src/util/junction-policies.ts
|
|
2625
|
-
var SERVER_OR_ADMIN_EXPR = policy.or(policy.serverContext(), policy.rolesOverlap(["admin"]));
|
|
2626
|
-
/**
|
|
2627
|
-
* Walk every collection's resolved relations and aggregate the junction tables
|
|
2628
|
-
* they declare. Two collections may declare the same junction from opposite
|
|
2629
|
-
* sides (posts→tags and tags→posts through `posts_tags`); both become
|
|
2630
|
-
* `declaringSides` of one spec, so derived write grants consider both.
|
|
2631
|
-
*/
|
|
2632
|
-
function resolveJunctionSpecs(collections) {
|
|
2633
|
-
const specs = /* @__PURE__ */ new Map();
|
|
2634
|
-
for (const collection of collections) {
|
|
2635
|
-
const resolved = resolveCollectionRelations(collection);
|
|
2636
|
-
for (const relation of Object.values(resolved)) {
|
|
2637
|
-
if (!relation.through) continue;
|
|
2638
|
-
const targetCollection = typeof relation.target === "function" ? relation.target() : void 0;
|
|
2639
|
-
if (!targetCollection) continue;
|
|
2640
|
-
const rawName = relation.through.table;
|
|
2641
|
-
const table = rawName.includes(".") ? rawName.split(".").pop() : rawName;
|
|
2642
|
-
const schema = "public";
|
|
2643
|
-
const source = {
|
|
2644
|
-
collection,
|
|
2645
|
-
junctionColumn: relation.through.sourceColumn,
|
|
2646
|
-
relation
|
|
2647
|
-
};
|
|
2648
|
-
const target = {
|
|
2649
|
-
collection: targetCollection,
|
|
2650
|
-
junctionColumn: relation.through.targetColumn
|
|
2651
|
-
};
|
|
2652
|
-
const existing = specs.get(table);
|
|
2653
|
-
if (!existing) specs.set(table, {
|
|
2654
|
-
table,
|
|
2655
|
-
schema,
|
|
2656
|
-
endpoints: [source, target],
|
|
2657
|
-
declaringSides: [source]
|
|
2658
|
-
});
|
|
2659
|
-
else if (!existing.declaringSides.some((s) => s.collection === collection)) existing.declaringSides.push(source);
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
return specs;
|
|
2663
|
-
}
|
|
2664
|
-
/**
|
|
2665
|
-
* A synthetic CollectionConfig standing in for the junction during policy
|
|
2666
|
-
* compilation and naming. Its two FK columns carry explicit `columnName`s so
|
|
2667
|
-
* `outerField` operands resolve to the exact columns the CREATE TABLE emitted,
|
|
2668
|
-
* whatever their casing.
|
|
2669
|
-
*/
|
|
2670
|
-
function getJunctionCollectionConfig(spec) {
|
|
2671
|
-
const properties = {};
|
|
2672
|
-
for (const endpoint of spec.endpoints) properties[endpoint.junctionColumn] = {
|
|
2673
|
-
type: "string",
|
|
2674
|
-
columnName: endpoint.junctionColumn
|
|
2675
|
-
};
|
|
2676
|
-
return {
|
|
2677
|
-
slug: spec.table,
|
|
2678
|
-
name: spec.table,
|
|
2679
|
-
table: spec.table,
|
|
2680
|
-
schema: spec.schema,
|
|
2681
|
-
properties
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
/** The property marked as the row id (falls back to `id`). */
|
|
2685
|
-
function getIdPropertyName(collection) {
|
|
2686
|
-
for (const [name, prop] of Object.entries(collection.properties ?? {})) if (prop && typeof prop === "object" && "isId" in prop && prop.isId) return name;
|
|
2687
|
-
return "id";
|
|
2688
|
-
}
|
|
2689
|
-
/** `EXISTS (SELECT 1 FROM endpoint WHERE endpoint.pk = junction.fk [AND extra])`. */
|
|
2690
|
-
function existsEndpoint(endpoint, extra) {
|
|
2691
|
-
const correlation = policy.compare(policy.field(getIdPropertyName(endpoint.collection)), "eq", policy.outerField(endpoint.junctionColumn));
|
|
2692
|
-
return policy.existsIn({
|
|
2693
|
-
collection: endpoint.collection.slug,
|
|
2694
|
-
where: extra ? policy.and(correlation, extra) : correlation
|
|
2695
|
-
});
|
|
2696
|
-
}
|
|
2697
|
-
/**
|
|
2698
|
-
* Whether a parent-rule expression keeps its meaning when moved inside the
|
|
2699
|
-
* junction's `EXISTS` subquery — and the re-scoped copy if it does.
|
|
2700
|
-
*
|
|
2701
|
-
* Returns `null` when the rule cannot be embedded faithfully: `raw` SQL
|
|
2702
|
-
* anywhere (its `{column}` placeholders would bind to the junction), or an
|
|
2703
|
-
* `outerField` inside a nested `existsIn` (it would bind to the junction while
|
|
2704
|
-
* the author meant the parent, and no operand can express "the middle scope").
|
|
2705
|
-
* Top-level `outerField`s are rewritten to `field`, which is what they meant.
|
|
2706
|
-
*/
|
|
2707
|
-
function embedParentExpression(expr, depth = 0) {
|
|
2708
|
-
switch (expr.kind) {
|
|
2709
|
-
case "raw": return null;
|
|
2710
|
-
case "and":
|
|
2711
|
-
case "or": {
|
|
2712
|
-
const parts = [];
|
|
2713
|
-
for (const child of expr.operands) {
|
|
2714
|
-
const embedded = embedParentExpression(child, depth);
|
|
2715
|
-
if (!embedded) return null;
|
|
2716
|
-
parts.push(embedded);
|
|
2717
|
-
}
|
|
2718
|
-
return expr.kind === "and" ? policy.and(...parts) : policy.or(...parts);
|
|
2719
|
-
}
|
|
2720
|
-
case "not": {
|
|
2721
|
-
const embedded = embedParentExpression(expr.operand, depth);
|
|
2722
|
-
return embedded ? policy.not(embedded) : null;
|
|
2723
|
-
}
|
|
2724
|
-
case "existsIn": {
|
|
2725
|
-
const where = embedParentExpression(expr.where, depth + 1);
|
|
2726
|
-
return where ? policy.existsIn({
|
|
2727
|
-
collection: expr.collection,
|
|
2728
|
-
where
|
|
2729
|
-
}) : null;
|
|
2730
|
-
}
|
|
2731
|
-
case "compare": {
|
|
2732
|
-
const left = embedOperand(expr.left, depth);
|
|
2733
|
-
const right = embedOperand(expr.right, depth);
|
|
2734
|
-
if (!left || !right) return null;
|
|
2735
|
-
return {
|
|
2736
|
-
...expr,
|
|
2737
|
-
left,
|
|
2738
|
-
right
|
|
2739
|
-
};
|
|
2740
|
-
}
|
|
2741
|
-
default: return expr;
|
|
2742
|
-
}
|
|
2743
|
-
}
|
|
2744
|
-
/** Re-scope an operand, or return `null` if its binding cannot be preserved. */
|
|
2745
|
-
function embedOperand(operand, depth) {
|
|
2746
|
-
if (operand.kind === "outerField") {
|
|
2747
|
-
if (depth === 0) return policy.field(operand.name);
|
|
2748
|
-
return null;
|
|
2749
|
-
}
|
|
2750
|
-
return operand;
|
|
2751
|
-
}
|
|
2752
|
-
/** Does the rule cover the `update` operation? */
|
|
2753
|
-
function coversUpdate(rule) {
|
|
2754
|
-
return getPolicyOperations(rule).some((op) => op === "update" || op === "all");
|
|
2755
|
-
}
|
|
2756
|
-
/**
|
|
2757
|
-
* The full derived policy set for a junction table: the locked server/admin
|
|
2758
|
-
* baseline, the endpoint-visibility read grant, inherited write grants, and
|
|
2759
|
-
* inherited restrictive gates. Returns `[]` when every declaring collection set
|
|
2760
|
-
* `disableDefaultPolicies` — the junction is then the author's to police, and
|
|
2761
|
-
* stays locked (RLS is still enabled) until they write policies for it.
|
|
2762
|
-
*/
|
|
2763
|
-
function getJunctionSecurityRules(spec) {
|
|
2764
|
-
if (spec.declaringSides.every((side) => side.collection.disableDefaultPolicies)) return [];
|
|
2765
|
-
const rules = [];
|
|
2766
|
-
rules.push({
|
|
2767
|
-
name: `${spec.table}_default_admin_read`,
|
|
2768
|
-
operations: ["select"],
|
|
2769
|
-
condition: SERVER_OR_ADMIN_EXPR
|
|
2770
|
-
});
|
|
2771
|
-
rules.push({
|
|
2772
|
-
name: `${spec.table}_default_admin_write`,
|
|
2773
|
-
operations: [
|
|
2774
|
-
"insert",
|
|
2775
|
-
"update",
|
|
2776
|
-
"delete"
|
|
2777
|
-
],
|
|
2778
|
-
condition: SERVER_OR_ADMIN_EXPR,
|
|
2779
|
-
check: SERVER_OR_ADMIN_EXPR
|
|
2780
|
-
});
|
|
2781
|
-
rules.push({
|
|
2782
|
-
name: `${spec.table}_default_edge_read`,
|
|
2783
|
-
operations: ["select"],
|
|
2784
|
-
condition: policy.and(existsEndpoint(spec.endpoints[0]), existsEndpoint(spec.endpoints[1]))
|
|
2785
|
-
});
|
|
2786
|
-
const writeGrants = [];
|
|
2787
|
-
for (const side of spec.declaringSides) {
|
|
2788
|
-
const updateRules = ((isPostgresCollectionConfig(side.collection) ? side.collection.securityRules : void 0) ?? []).filter(coversUpdate);
|
|
2789
|
-
const permissive = updateRules.filter((r) => r.mode !== "restrictive");
|
|
2790
|
-
const restrictive = updateRules.filter((r) => r.mode === "restrictive");
|
|
2791
|
-
const embeddedGates = [];
|
|
2792
|
-
let gatesEmbeddable = true;
|
|
2793
|
-
for (const gate of restrictive) {
|
|
2794
|
-
const using = securityRuleToConditions(gate).usingExpr;
|
|
2795
|
-
const embedded = using ? embedParentExpression(using) : null;
|
|
2796
|
-
if (!embedded) {
|
|
2797
|
-
gatesEmbeddable = false;
|
|
2798
|
-
break;
|
|
2799
|
-
}
|
|
2800
|
-
embeddedGates.push(embedded);
|
|
2801
|
-
}
|
|
2802
|
-
if (!gatesEmbeddable) continue;
|
|
2803
|
-
const grants = [];
|
|
2804
|
-
for (const rule of permissive) {
|
|
2805
|
-
const using = securityRuleToConditions(rule).usingExpr;
|
|
2806
|
-
const embedded = using ? embedParentExpression(using) : null;
|
|
2807
|
-
if (embedded) grants.push(embedded);
|
|
2808
|
-
}
|
|
2809
|
-
if (grants.length === 0) continue;
|
|
2810
|
-
const condition = embeddedGates.length > 0 ? policy.and(policy.or(...grants), ...embeddedGates) : policy.or(...grants);
|
|
2811
|
-
writeGrants.push(existsEndpoint(side, condition));
|
|
2812
|
-
}
|
|
2813
|
-
if (writeGrants.length > 0) rules.push({
|
|
2814
|
-
name: `${spec.table}_default_edge_write`,
|
|
2815
|
-
operations: [
|
|
2816
|
-
"insert",
|
|
2817
|
-
"update",
|
|
2818
|
-
"delete"
|
|
2819
|
-
],
|
|
2820
|
-
condition: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants),
|
|
2821
|
-
check: writeGrants.length === 1 ? writeGrants[0] : policy.or(...writeGrants)
|
|
2822
|
-
});
|
|
2823
|
-
return rules;
|
|
2824
|
-
}
|
|
2825
2612
|
(/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2826
2613
|
(function(root, factory) {
|
|
2827
2614
|
if (typeof define === "function" && define.amd) define(factory);
|
|
@@ -4106,7 +3893,7 @@ function createPrimaryKeyResolver(options) {
|
|
|
4106
3893
|
}
|
|
4107
3894
|
if (!warned.has(slug)) {
|
|
4108
3895
|
warned.add(slug);
|
|
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
|
|
3896
|
+
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.`);
|
|
4110
3897
|
}
|
|
4111
3898
|
return keys;
|
|
4112
3899
|
};
|
|
@@ -4549,26 +4336,8 @@ function getTableForCollection(collection, registry) {
|
|
|
4549
4336
|
if (!table) throw new Error(`Table not found for collection '${collection.slug}' (table: ${tableName})`);
|
|
4550
4337
|
return table;
|
|
4551
4338
|
}
|
|
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
|
-
*/
|
|
4571
4339
|
function getPrimaryKeys(collection, registry) {
|
|
4340
|
+
const table = getTableForCollection(collection, registry);
|
|
4572
4341
|
if (collection.properties) {
|
|
4573
4342
|
const idProps = Object.entries(collection.properties).filter(([_, prop]) => "isId" in prop && Boolean(prop.isId)).map(([key, prop]) => ({
|
|
4574
4343
|
fieldName: key,
|
|
@@ -4577,8 +4346,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4577
4346
|
}));
|
|
4578
4347
|
if (idProps.length > 0) return idProps;
|
|
4579
4348
|
}
|
|
4580
|
-
const table = registry.getTable(getTableName$1(collection));
|
|
4581
|
-
if (!table) return [];
|
|
4582
4349
|
const keys = [];
|
|
4583
4350
|
for (const [key, colRaw] of Object.entries(table)) {
|
|
4584
4351
|
const col = colRaw;
|
|
@@ -4606,90 +4373,6 @@ function getPrimaryKeys(collection, registry) {
|
|
|
4606
4373
|
}
|
|
4607
4374
|
return keys;
|
|
4608
4375
|
}
|
|
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
|
-
}
|
|
4693
4376
|
//#endregion
|
|
4694
4377
|
//#region src/utils/drizzle-conditions.ts
|
|
4695
4378
|
/**
|
|
@@ -5311,10 +4994,12 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5311
4994
|
continue;
|
|
5312
4995
|
} else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
5313
4996
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
4997
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
5314
4998
|
inverseRelationUpdates.push({
|
|
5315
4999
|
relationKey: key,
|
|
5316
5000
|
relation,
|
|
5317
|
-
newValue: serializedValue
|
|
5001
|
+
newValue: serializedValue,
|
|
5002
|
+
currentId: row.id || buildCompositeId(row, pks)
|
|
5318
5003
|
});
|
|
5319
5004
|
continue;
|
|
5320
5005
|
} else if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
@@ -5324,11 +5009,15 @@ function serializeDataToServer(row, properties, collection, registry) {
|
|
|
5324
5009
|
relation,
|
|
5325
5010
|
newTargetId: serializedValue
|
|
5326
5011
|
});
|
|
5327
|
-
else
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5012
|
+
else {
|
|
5013
|
+
const pks = getPrimaryKeys(collection, registry);
|
|
5014
|
+
inverseRelationUpdates.push({
|
|
5015
|
+
relationKey: key,
|
|
5016
|
+
relation,
|
|
5017
|
+
newValue: serializedValue,
|
|
5018
|
+
currentId: row.id || buildCompositeId(row, pks)
|
|
5019
|
+
});
|
|
5020
|
+
}
|
|
5332
5021
|
continue;
|
|
5333
5022
|
} else if (relation.cardinality === "one" && relation.direction === "owning" && relation.joinPath && relation.joinPath.length > 0) {
|
|
5334
5023
|
const serializedValue = serializePropertyToServer(effectiveValue, property);
|
|
@@ -5728,63 +5417,6 @@ var RelationService = class {
|
|
|
5728
5417
|
this.registry = registry;
|
|
5729
5418
|
}
|
|
5730
5419
|
/**
|
|
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
|
-
/**
|
|
5788
5420
|
* Fetch rows related to a parent row through a specific relation
|
|
5789
5421
|
*/
|
|
5790
5422
|
async fetchRelatedEntities(parentCollectionPath, parentId, relationKey, options = {}) {
|
|
@@ -5803,9 +5435,9 @@ var RelationService = class {
|
|
|
5803
5435
|
async fetchEntitiesUsingJoins(parentCollection, parentId, relation, options = {}) {
|
|
5804
5436
|
const targetCollection = relation.target();
|
|
5805
5437
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5806
|
-
const idInfo =
|
|
5438
|
+
const idInfo = getPrimaryKeys(targetCollection, this.registry);
|
|
5807
5439
|
const idField = targetTable[idInfo[0].fieldName];
|
|
5808
|
-
const parentPks =
|
|
5440
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5809
5441
|
const parentIdInfo = parentPks[0];
|
|
5810
5442
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5811
5443
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5829,7 +5461,7 @@ var RelationService = class {
|
|
|
5829
5461
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5830
5462
|
currentTable = joinTable;
|
|
5831
5463
|
}
|
|
5832
|
-
const parentIdField = parentTable[
|
|
5464
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5833
5465
|
query = query.where(eq(parentIdField, parsedParentId));
|
|
5834
5466
|
if (options.limit) query = query.limit(options.limit);
|
|
5835
5467
|
const results = await query;
|
|
@@ -5837,7 +5469,13 @@ var RelationService = class {
|
|
|
5837
5469
|
const rows = [];
|
|
5838
5470
|
for (const row of results) {
|
|
5839
5471
|
const targetRow = row[targetTableName] || row;
|
|
5840
|
-
|
|
5472
|
+
const id = targetRow[idInfo[0].fieldName];
|
|
5473
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5474
|
+
rows.push({
|
|
5475
|
+
id: id?.toString() || "",
|
|
5476
|
+
path: targetCollection.slug,
|
|
5477
|
+
values: parsedValues
|
|
5478
|
+
});
|
|
5841
5479
|
}
|
|
5842
5480
|
return rows;
|
|
5843
5481
|
}
|
|
@@ -5856,7 +5494,13 @@ var RelationService = class {
|
|
|
5856
5494
|
const rows = [];
|
|
5857
5495
|
for (const row of results) {
|
|
5858
5496
|
const targetRow = row[getTableName$1(targetCollection)] || row;
|
|
5859
|
-
|
|
5497
|
+
const id = targetRow[idInfo[0].fieldName];
|
|
5498
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection, this.db, this.registry);
|
|
5499
|
+
rows.push({
|
|
5500
|
+
id: id?.toString() || "",
|
|
5501
|
+
path: targetCollection.slug,
|
|
5502
|
+
values: parsedValues
|
|
5503
|
+
});
|
|
5860
5504
|
}
|
|
5861
5505
|
return rows;
|
|
5862
5506
|
}
|
|
@@ -5873,8 +5517,8 @@ var RelationService = class {
|
|
|
5873
5517
|
}
|
|
5874
5518
|
const targetCollection = relation.target();
|
|
5875
5519
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5876
|
-
const targetIdField = targetTable[
|
|
5877
|
-
const parentPks =
|
|
5520
|
+
const targetIdField = targetTable[getPrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
5521
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5878
5522
|
const parentIdInfo = parentPks[0];
|
|
5879
5523
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
5880
5524
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
@@ -5893,10 +5537,9 @@ var RelationService = class {
|
|
|
5893
5537
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5894
5538
|
const targetCollection = relation.target();
|
|
5895
5539
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5896
|
-
const
|
|
5897
|
-
const targetIdInfo = targetPks[0];
|
|
5540
|
+
const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
|
|
5898
5541
|
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5899
|
-
const parentPks =
|
|
5542
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5900
5543
|
const parentIdInfo = parentPks[0];
|
|
5901
5544
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5902
5545
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -5920,19 +5563,25 @@ var RelationService = class {
|
|
|
5920
5563
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
5921
5564
|
currentTable = joinTable;
|
|
5922
5565
|
}
|
|
5923
|
-
|
|
5566
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5567
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
5924
5568
|
const results = await query;
|
|
5925
5569
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
5926
5570
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5927
5571
|
for (const row of results) {
|
|
5928
5572
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
5929
5573
|
const targetRow = row[targetTableName] || row;
|
|
5930
|
-
|
|
5574
|
+
const parentId = parentRow[parentIdInfo.fieldName];
|
|
5575
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5576
|
+
resultMap.set(String(parentId), {
|
|
5577
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5578
|
+
path: targetCollection.slug,
|
|
5579
|
+
values: parsedValues
|
|
5580
|
+
});
|
|
5931
5581
|
}
|
|
5932
5582
|
return resultMap;
|
|
5933
5583
|
}
|
|
5934
5584
|
if (relation.direction === "owning" && relation.localKey) {
|
|
5935
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.localKey);
|
|
5936
5585
|
const localKeyCol = parentTable[relation.localKey];
|
|
5937
5586
|
if (!localKeyCol) throw new Error(`Local key column '${relation.localKey}' not found in parent table`);
|
|
5938
5587
|
const fkRows = await this.db.select({
|
|
@@ -5961,11 +5610,17 @@ var RelationService = class {
|
|
|
5961
5610
|
const resultMap = /* @__PURE__ */ new Map();
|
|
5962
5611
|
for (const [parentIdStr, fkValue] of parentToFk) {
|
|
5963
5612
|
const targetRow = targetById.get(String(fkValue));
|
|
5964
|
-
if (targetRow)
|
|
5613
|
+
if (targetRow) {
|
|
5614
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5615
|
+
resultMap.set(parentIdStr, {
|
|
5616
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5617
|
+
path: targetCollection.slug,
|
|
5618
|
+
values: parsedValues
|
|
5619
|
+
});
|
|
5620
|
+
}
|
|
5965
5621
|
}
|
|
5966
5622
|
return resultMap;
|
|
5967
5623
|
}
|
|
5968
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
5969
5624
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
5970
5625
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
5971
5626
|
const results = await query;
|
|
@@ -5976,7 +5631,14 @@ var RelationService = class {
|
|
|
5976
5631
|
let parentId;
|
|
5977
5632
|
if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
5978
5633
|
else if (relation.direction === "inverse" && relation.cardinality === "one" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
5979
|
-
if (parentId !== void 0 && parentIdSet.has(String(parentId)))
|
|
5634
|
+
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5635
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
5636
|
+
resultMap.set(String(parentId), {
|
|
5637
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5638
|
+
path: targetCollection.slug,
|
|
5639
|
+
values: parsedValues
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5980
5642
|
}
|
|
5981
5643
|
return resultMap;
|
|
5982
5644
|
}
|
|
@@ -5990,9 +5652,9 @@ var RelationService = class {
|
|
|
5990
5652
|
const parentCollection = getCollectionByPath(parentCollectionPath, this.registry);
|
|
5991
5653
|
const targetCollection = relation.target();
|
|
5992
5654
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
5993
|
-
const
|
|
5994
|
-
const targetIdField = targetTable[
|
|
5995
|
-
const parentPks =
|
|
5655
|
+
const targetIdInfo = getPrimaryKeys(targetCollection, this.registry)[0];
|
|
5656
|
+
const targetIdField = targetTable[targetIdInfo.fieldName];
|
|
5657
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
5996
5658
|
const parentIdInfo = parentPks[0];
|
|
5997
5659
|
const parentTable = this.registry.getTable(getTableName$1(parentCollection));
|
|
5998
5660
|
if (!parentTable) throw new Error("Parent table not found");
|
|
@@ -6014,22 +5676,27 @@ var RelationService = class {
|
|
|
6014
5676
|
query = applyDynamicJoin(query, joinTable, eq(fromCol, toCol));
|
|
6015
5677
|
currentTable = joinTable;
|
|
6016
5678
|
}
|
|
6017
|
-
|
|
5679
|
+
const parentIdField = parentTable[getPrimaryKeys(parentCollection, this.registry)[0].fieldName];
|
|
5680
|
+
query = query.where(inArray(parentIdField, parsedParentIds));
|
|
6018
5681
|
const results = await query;
|
|
6019
5682
|
const targetTableName = relation.joinPath[relation.joinPath.length - 1].table;
|
|
6020
5683
|
const resultMap = /* @__PURE__ */ new Map();
|
|
6021
5684
|
for (const row of results) {
|
|
6022
5685
|
const parentRow = row[getTableName$1(parentCollection)] || row;
|
|
6023
5686
|
const targetRow = row[targetTableName] || row;
|
|
6024
|
-
const parentId =
|
|
5687
|
+
const parentId = String(parentRow[parentIdInfo.fieldName]);
|
|
5688
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6025
5689
|
const arr = resultMap.get(parentId) || [];
|
|
6026
|
-
arr.push(
|
|
5690
|
+
arr.push({
|
|
5691
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5692
|
+
path: targetCollection.slug,
|
|
5693
|
+
values: parsedValues
|
|
5694
|
+
});
|
|
6027
5695
|
resultMap.set(parentId, arr);
|
|
6028
5696
|
}
|
|
6029
5697
|
return resultMap;
|
|
6030
5698
|
}
|
|
6031
5699
|
if (relation.through && relation.cardinality === "many" && relation.direction === "owning") {
|
|
6032
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, `${relation.through.table}.${relation.through.sourceColumn}`);
|
|
6033
5700
|
const junctionTable = this.registry.getTable(relation.through.table);
|
|
6034
5701
|
if (!junctionTable) {
|
|
6035
5702
|
logger.warn(`[batchFetchRelatedEntitiesMany] Junction table '${relation.through.table}' not found`);
|
|
@@ -6048,13 +5715,17 @@ var RelationService = class {
|
|
|
6048
5715
|
const junctionData = row[relation.through.table] || row;
|
|
6049
5716
|
const targetData = row[targetTableName] || row;
|
|
6050
5717
|
const parentId = String(junctionData[relation.through.sourceColumn]);
|
|
5718
|
+
const parsedValues = await parseDataFromServer(targetData, targetCollection);
|
|
6051
5719
|
const arr = resultMap.get(parentId) || [];
|
|
6052
|
-
arr.push(
|
|
5720
|
+
arr.push({
|
|
5721
|
+
id: String(targetData[targetIdInfo.fieldName]),
|
|
5722
|
+
path: targetCollection.slug,
|
|
5723
|
+
values: parsedValues
|
|
5724
|
+
});
|
|
6053
5725
|
resultMap.set(parentId, arr);
|
|
6054
5726
|
}
|
|
6055
5727
|
return resultMap;
|
|
6056
5728
|
}
|
|
6057
|
-
this.assertSingleKeyAddressable(parentCollection, parentPks, relation.foreignKeyOnTarget ?? `${relation.inverseRelationName}_id`);
|
|
6058
5729
|
let query = this.db.select().from(targetTable).$dynamic();
|
|
6059
5730
|
query = applyDynamicRelationQuery(query, query, relation, parsedParentIds, targetTable, parentTable, parentIdCol, targetIdField, this.registry, []);
|
|
6060
5731
|
const results = await query;
|
|
@@ -6067,9 +5738,14 @@ var RelationService = class {
|
|
|
6067
5738
|
else if (relation.direction === "inverse" && relation.foreignKeyOnTarget) parentId = targetRow[relation.foreignKeyOnTarget];
|
|
6068
5739
|
else if (relation.direction === "inverse" && relation.inverseRelationName) parentId = targetRow[`${relation.inverseRelationName}_id`];
|
|
6069
5740
|
if (parentId !== void 0 && parentIdSet.has(String(parentId))) {
|
|
5741
|
+
const parsedValues = await parseDataFromServer(targetRow, targetCollection);
|
|
6070
5742
|
const key = String(parentId);
|
|
6071
5743
|
const arr = resultMap.get(key) || [];
|
|
6072
|
-
arr.push(
|
|
5744
|
+
arr.push({
|
|
5745
|
+
id: String(targetRow[targetIdInfo.fieldName]),
|
|
5746
|
+
path: targetCollection.slug,
|
|
5747
|
+
values: parsedValues
|
|
5748
|
+
});
|
|
6073
5749
|
resultMap.set(key, arr);
|
|
6074
5750
|
}
|
|
6075
5751
|
}
|
|
@@ -6117,12 +5793,12 @@ var RelationService = class {
|
|
|
6117
5793
|
logger.warn(`Could not determine junction table for relation '${key}' in collection '${collection.slug}'`);
|
|
6118
5794
|
continue;
|
|
6119
5795
|
}
|
|
6120
|
-
const parentPks =
|
|
5796
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6121
5797
|
const parentIdInfo = parentPks[0];
|
|
6122
5798
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6123
5799
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6124
5800
|
if (targetEntityIds.length > 0) {
|
|
6125
|
-
const targetPks =
|
|
5801
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6126
5802
|
const targetIdInfo = targetPks[0];
|
|
6127
5803
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6128
5804
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6142,12 +5818,12 @@ var RelationService = class {
|
|
|
6142
5818
|
logger.warn(`Junction columns not found for relation '${key}'`);
|
|
6143
5819
|
continue;
|
|
6144
5820
|
}
|
|
6145
|
-
const parentPks =
|
|
5821
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6146
5822
|
const parentIdInfo = parentPks[0];
|
|
6147
5823
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6148
5824
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedParentId));
|
|
6149
5825
|
if (targetEntityIds.length > 0) {
|
|
6150
|
-
const targetPks =
|
|
5826
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6151
5827
|
const targetIdInfo = targetPks[0];
|
|
6152
5828
|
const newLinks = targetEntityIds.map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6153
5829
|
[sourceJunctionColumn.name]: parsedParentId,
|
|
@@ -6158,7 +5834,7 @@ var RelationService = class {
|
|
|
6158
5834
|
} 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.`);
|
|
6159
5835
|
else if (relation.cardinality === "many" && relation.direction === "inverse" && relation.foreignKeyOnTarget) {
|
|
6160
5836
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6161
|
-
const targetPks =
|
|
5837
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6162
5838
|
const targetIdInfo = targetPks[0];
|
|
6163
5839
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6164
5840
|
const fkCol = targetTable[relation.foreignKeyOnTarget];
|
|
@@ -6166,7 +5842,7 @@ var RelationService = class {
|
|
|
6166
5842
|
logger.warn(`Invalid inverse-many config for relation '${key}' in collection '${collection.slug}'`);
|
|
6167
5843
|
continue;
|
|
6168
5844
|
}
|
|
6169
|
-
const parentPks =
|
|
5845
|
+
const parentPks = getPrimaryKeys(collection, this.registry);
|
|
6170
5846
|
const parentIdInfo = parentPks[0];
|
|
6171
5847
|
const parsedParentId = parseIdValues(id, parentPks)[parentIdInfo.fieldName];
|
|
6172
5848
|
if (targetEntityIds.length > 0) {
|
|
@@ -6186,9 +5862,9 @@ var RelationService = class {
|
|
|
6186
5862
|
try {
|
|
6187
5863
|
const targetCollection = relation.target();
|
|
6188
5864
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6189
|
-
const targetPks =
|
|
5865
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6190
5866
|
const targetIdInfo = targetPks[0];
|
|
6191
|
-
const sourcePks =
|
|
5867
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6192
5868
|
const sourceIdInfo = sourcePks[0];
|
|
6193
5869
|
if (relation.direction === "inverse" && relation.joinPath && relation.joinPath.length > 0) {
|
|
6194
5870
|
await this.updateInverseJoinPathRelation(tx, sourceCollection, sourceEntityId, targetCollection, relation, newValue);
|
|
@@ -6269,12 +5945,12 @@ var RelationService = class {
|
|
|
6269
5945
|
logger.warn(`Could not determine junction columns for inverse joinPath relation '${relation.relationName}'`);
|
|
6270
5946
|
return;
|
|
6271
5947
|
}
|
|
6272
|
-
const sourcePks =
|
|
5948
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6273
5949
|
const sourceIdInfo = sourcePks[0];
|
|
6274
5950
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6275
5951
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6276
5952
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6277
|
-
const targetPks =
|
|
5953
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6278
5954
|
const targetIdInfo = targetPks[0];
|
|
6279
5955
|
const newLinks = newValue.map((rel) => typeof rel === "object" && rel !== null ? rel.id : rel).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6280
5956
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6282,7 +5958,7 @@ var RelationService = class {
|
|
|
6282
5958
|
}));
|
|
6283
5959
|
if (newLinks.length > 0) await tx.insert(junctionTable).values(newLinks);
|
|
6284
5960
|
} else if (newValue && !Array.isArray(newValue)) {
|
|
6285
|
-
const targetPks =
|
|
5961
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6286
5962
|
const targetIdInfo = targetPks[0];
|
|
6287
5963
|
const parsedTargetId = parseIdValues(typeof newValue === "object" && newValue !== null ? newValue.id : newValue, targetPks)[targetIdInfo.fieldName];
|
|
6288
5964
|
const newLink = {
|
|
@@ -6313,12 +5989,12 @@ var RelationService = class {
|
|
|
6313
5989
|
logger.warn(`Junction columns not found for relation '${relation.relationName}'`);
|
|
6314
5990
|
return;
|
|
6315
5991
|
}
|
|
6316
|
-
const sourcePks =
|
|
5992
|
+
const sourcePks = getPrimaryKeys(sourceCollection, this.registry);
|
|
6317
5993
|
const sourceIdInfo = sourcePks[0];
|
|
6318
5994
|
const parsedSourceId = parseIdValues(sourceEntityId, sourcePks)[sourceIdInfo.fieldName];
|
|
6319
5995
|
await tx.delete(junctionTable).where(eq(sourceJunctionColumn, parsedSourceId));
|
|
6320
5996
|
if (newValue && Array.isArray(newValue) && newValue.length > 0) {
|
|
6321
|
-
const targetPks =
|
|
5997
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6322
5998
|
const targetIdInfo = targetPks[0];
|
|
6323
5999
|
const newLinks = newValue.map((rel) => rel.id).map((id) => parseIdValues(id, targetPks)[targetIdInfo.fieldName]).map((targetId) => ({
|
|
6324
6000
|
[sourceJunctionColumn.name]: parsedSourceId,
|
|
@@ -6339,12 +6015,12 @@ var RelationService = class {
|
|
|
6339
6015
|
const { relation, newTargetId } = upd;
|
|
6340
6016
|
const targetCollection = relation.target();
|
|
6341
6017
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
6342
|
-
const targetPks =
|
|
6018
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6343
6019
|
const targetIdInfo = targetPks[0];
|
|
6344
6020
|
const targetIdCol = targetTable[targetIdInfo.fieldName];
|
|
6345
6021
|
const { targetFKColName, parentSourceColName } = this.resolveJoinPathWriteMapping(parentCollection, relation);
|
|
6346
6022
|
const parentTable = getTableForCollection(parentCollection, this.registry);
|
|
6347
|
-
const parentPks =
|
|
6023
|
+
const parentPks = getPrimaryKeys(parentCollection, this.registry);
|
|
6348
6024
|
const parentIdInfo = parentPks[0];
|
|
6349
6025
|
const parsedParentId = parseIdValues(parentId, parentPks)[parentIdInfo.fieldName];
|
|
6350
6026
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
@@ -6415,7 +6091,7 @@ var RelationService = class {
|
|
|
6415
6091
|
logger.warn(`Junction columns not found for relation '${relationKey}'`);
|
|
6416
6092
|
return;
|
|
6417
6093
|
}
|
|
6418
|
-
const targetPks =
|
|
6094
|
+
const targetPks = getPrimaryKeys(targetCollection, this.registry);
|
|
6419
6095
|
const targetIdInfo = targetPks[0];
|
|
6420
6096
|
const parsedNewEntityId = parseIdValues(newEntityId, targetPks)[targetIdInfo.fieldName];
|
|
6421
6097
|
const junctionData = {
|
|
@@ -6431,100 +6107,6 @@ var RelationService = class {
|
|
|
6431
6107
|
}
|
|
6432
6108
|
};
|
|
6433
6109
|
//#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
|
|
6528
6110
|
//#region src/services/FetchService.ts
|
|
6529
6111
|
/**
|
|
6530
6112
|
* Service for handling all row read operations.
|
|
@@ -6583,7 +6165,7 @@ var FetchService = class {
|
|
|
6583
6165
|
if (!shouldInclude(key)) continue;
|
|
6584
6166
|
const drizzleRelName = relation.relationName || key;
|
|
6585
6167
|
if (relation.joinPath && relation.joinPath.length > 0) continue;
|
|
6586
|
-
if (relation.cardinality === "many" && isJunctionRelation(relation)) {
|
|
6168
|
+
if (relation.cardinality === "many" && this.isJunctionRelation(relation, collection)) {
|
|
6587
6169
|
const targetFkName = this.getJunctionTargetRelationName(relation, collection);
|
|
6588
6170
|
if (targetFkName) withConfig[drizzleRelName] = { with: { [targetFkName]: true } };
|
|
6589
6171
|
else withConfig[drizzleRelName] = true;
|
|
@@ -6592,6 +6174,14 @@ var FetchService = class {
|
|
|
6592
6174
|
return withConfig;
|
|
6593
6175
|
}
|
|
6594
6176
|
/**
|
|
6177
|
+
* Detect if a many-to-many relation uses a junction table in the Drizzle schema.
|
|
6178
|
+
*/
|
|
6179
|
+
isJunctionRelation(relation, _collection) {
|
|
6180
|
+
if (relation.through) return true;
|
|
6181
|
+
if (relation.joinPath && relation.joinPath.length > 1) return true;
|
|
6182
|
+
return false;
|
|
6183
|
+
}
|
|
6184
|
+
/**
|
|
6595
6185
|
* Get the Drizzle relation name on the junction table that points to the actual target row.
|
|
6596
6186
|
* For example, for posts_tags junction, this returns "tag_id" (the relation pointing to tags).
|
|
6597
6187
|
*/
|
|
@@ -6600,6 +6190,54 @@ var FetchService = class {
|
|
|
6600
6190
|
return null;
|
|
6601
6191
|
}
|
|
6602
6192
|
/**
|
|
6193
|
+
* Convert a db.query result row (with nested relation objects) to a flat row.
|
|
6194
|
+
* Handles:
|
|
6195
|
+
* - Type normalization (dates, numbers, NaN) via normalizeDbValues
|
|
6196
|
+
* - Converting nested relation objects to { id, path, __type: "relation" } for CMS
|
|
6197
|
+
* - Flattening junction-table many-to-many results
|
|
6198
|
+
*
|
|
6199
|
+
* The row's own address is not among them: it is derived by the consumer
|
|
6200
|
+
* from the collection's primary keys.
|
|
6201
|
+
*/
|
|
6202
|
+
drizzleResultToRow(row, collection) {
|
|
6203
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6204
|
+
const normalizedValues = normalizeDbValues(row, collection);
|
|
6205
|
+
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
6206
|
+
const relData = row[relation.relationName || key];
|
|
6207
|
+
if (relData === void 0 || relData === null) continue;
|
|
6208
|
+
if (relation.cardinality === "many" && Array.isArray(relData)) {
|
|
6209
|
+
const targetCollection = relation.target();
|
|
6210
|
+
const targetPath = targetCollection.slug;
|
|
6211
|
+
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
6212
|
+
normalizedValues[key] = relData.map((item) => {
|
|
6213
|
+
let targetRow = item;
|
|
6214
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6215
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6216
|
+
if (nestedKey) targetRow = item[nestedKey];
|
|
6217
|
+
}
|
|
6218
|
+
const relId = String(targetRow[targetIdField] ?? targetRow.id ?? targetRow[Object.keys(targetRow)[0]]);
|
|
6219
|
+
return createRelationRefWithData(relId, targetPath, {
|
|
6220
|
+
id: relId,
|
|
6221
|
+
path: targetPath,
|
|
6222
|
+
values: normalizeDbValues(targetRow, targetCollection)
|
|
6223
|
+
});
|
|
6224
|
+
});
|
|
6225
|
+
} else if (relation.cardinality === "one" && typeof relData === "object" && !Array.isArray(relData)) {
|
|
6226
|
+
const targetCollection = relation.target();
|
|
6227
|
+
const targetPath = targetCollection.slug;
|
|
6228
|
+
const targetIdField = getPrimaryKeys(targetCollection, this.registry)[0].fieldName;
|
|
6229
|
+
const relObj = relData;
|
|
6230
|
+
const relId = String(relObj[targetIdField] ?? relObj.id ?? relObj[Object.keys(relObj)[0]]);
|
|
6231
|
+
normalizedValues[key] = createRelationRefWithData(relId, targetPath, {
|
|
6232
|
+
id: relId,
|
|
6233
|
+
path: targetPath,
|
|
6234
|
+
values: normalizeDbValues(relObj, targetCollection)
|
|
6235
|
+
});
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
return normalizedValues;
|
|
6239
|
+
}
|
|
6240
|
+
/**
|
|
6603
6241
|
* Post-fetch joinPath relations for a single flat row.
|
|
6604
6242
|
* joinPath relations cannot be expressed via Drizzle's `with` config,
|
|
6605
6243
|
* so they must be loaded separately after the primary query.
|
|
@@ -6620,6 +6258,31 @@ var FetchService = class {
|
|
|
6620
6258
|
await Promise.all(promises);
|
|
6621
6259
|
}
|
|
6622
6260
|
/**
|
|
6261
|
+
* Post-fetch joinPath relations for a batch of flat rows.
|
|
6262
|
+
* Uses batch fetching to avoid N+1 queries for list views.
|
|
6263
|
+
*/
|
|
6264
|
+
async resolveJoinPathRelationsBatch(rows, collection, collectionPath, idInfo, _databaseId) {
|
|
6265
|
+
if (rows.length === 0) return;
|
|
6266
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6267
|
+
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0);
|
|
6268
|
+
if (joinPathRelations.length === 0) return;
|
|
6269
|
+
for (const [key, relation] of joinPathRelations) try {
|
|
6270
|
+
const rowIds = rows.map((r) => {
|
|
6271
|
+
return parseIdValues(String(r.id), [idInfo])[idInfo.fieldName];
|
|
6272
|
+
});
|
|
6273
|
+
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6274
|
+
for (const row of rows) {
|
|
6275
|
+
const id = parseIdValues(String(row.id), [idInfo])[idInfo.fieldName];
|
|
6276
|
+
const relatedRow = resultMap.get(String(id));
|
|
6277
|
+
if (relatedRow) {
|
|
6278
|
+
if (relation.cardinality === "one") row[key] = createRelationRefWithData(relatedRow.id, relatedRow.path, relatedRow);
|
|
6279
|
+
}
|
|
6280
|
+
}
|
|
6281
|
+
} catch (e) {
|
|
6282
|
+
logger.warn(`Could not batch resolve joinPath relation '${key}'`, { error: e });
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
/**
|
|
6623
6286
|
* Resolves joinPath relations for raw REST rows and directly injects them.
|
|
6624
6287
|
* Uses RelationService to query the database and maps results back to the flattened objects.
|
|
6625
6288
|
*/
|
|
@@ -6630,29 +6293,63 @@ var FetchService = class {
|
|
|
6630
6293
|
const shouldInclude = (key) => !include || include.length === 0 || include[0] === "*" || include.includes(key);
|
|
6631
6294
|
const joinPathRelations = Object.entries(resolvedRelations).filter(([key, relation]) => relation.joinPath && relation.joinPath.length > 0 && propertyKeys.has(key) && shouldInclude(key));
|
|
6632
6295
|
if (joinPathRelations.length === 0) return;
|
|
6633
|
-
const
|
|
6634
|
-
const address = buildCompositeId(row, idInfoArray);
|
|
6635
|
-
return address && address.split(":::").some((part) => part !== "") ? address : void 0;
|
|
6636
|
-
};
|
|
6296
|
+
const idInfo = idInfoArray[0];
|
|
6637
6297
|
for (const [key, relation] of joinPathRelations) try {
|
|
6638
|
-
const
|
|
6639
|
-
|
|
6640
|
-
|
|
6298
|
+
const rowIds = rows.map((r) => {
|
|
6299
|
+
return parseIdValues(String(r.id), idInfoArray)[idInfo.fieldName];
|
|
6300
|
+
});
|
|
6641
6301
|
if (relation.cardinality === "one") {
|
|
6642
6302
|
const resultMap = await this.relationService.batchFetchRelatedEntities(collectionPath, rowIds, key, relation);
|
|
6643
|
-
for (const row of
|
|
6644
|
-
const
|
|
6645
|
-
|
|
6303
|
+
for (const row of rows) {
|
|
6304
|
+
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
6305
|
+
const relatedRow = resultMap.get(String(id));
|
|
6306
|
+
if (relatedRow) row[key] = {
|
|
6307
|
+
...relatedRow.values,
|
|
6308
|
+
id: relatedRow.id
|
|
6309
|
+
};
|
|
6310
|
+
else row[key] = null;
|
|
6646
6311
|
}
|
|
6647
6312
|
} else if (relation.cardinality === "many") {
|
|
6648
6313
|
const resultMap = await this.relationService.batchFetchRelatedEntitiesMany(collectionPath, rowIds, key, relation);
|
|
6649
|
-
for (const row of
|
|
6314
|
+
for (const row of rows) {
|
|
6315
|
+
const id = parseIdValues(String(row.id), idInfoArray)[idInfo.fieldName];
|
|
6316
|
+
row[key] = (resultMap.get(String(id)) || []).map((e) => ({
|
|
6317
|
+
...e.values,
|
|
6318
|
+
id: e.id
|
|
6319
|
+
}));
|
|
6320
|
+
}
|
|
6650
6321
|
}
|
|
6651
6322
|
} catch (e) {
|
|
6652
6323
|
logger.warn(`Could not batch resolve joinPath relation '${key}' for REST`, { error: e });
|
|
6653
6324
|
}
|
|
6654
6325
|
}
|
|
6655
6326
|
/**
|
|
6327
|
+
* Convert a db.query result row to a flat REST-style row with populated relations.
|
|
6328
|
+
*
|
|
6329
|
+
* Every column is copied through under its own name, with the value Postgres
|
|
6330
|
+
* returned. This used to open with a synthesized `id` and then skip the key
|
|
6331
|
+
* column, which renamed it (a `sku` primary key was served as `id`, and `sku`
|
|
6332
|
+
* did not appear at all) and restringified it (`42` → `"42"`). Consumers that
|
|
6333
|
+
* need an address derive it from the collection's primary keys.
|
|
6334
|
+
*/
|
|
6335
|
+
drizzleResultToRestRow(row, collection) {
|
|
6336
|
+
const flat = {};
|
|
6337
|
+
const resolvedRelations = resolveCollectionRelations(collection);
|
|
6338
|
+
for (const [k, v] of Object.entries(row)) {
|
|
6339
|
+
const relation = findRelation(resolvedRelations, k);
|
|
6340
|
+
if (Array.isArray(v) && relation) flat[k] = v.map((item) => {
|
|
6341
|
+
if (this.isJunctionRelation(relation, collection)) {
|
|
6342
|
+
const nestedKey = Object.keys(item).find((nk) => typeof item[nk] === "object" && item[nk] !== null && !Array.isArray(item[nk]));
|
|
6343
|
+
if (nestedKey) return { ...item[nestedKey] };
|
|
6344
|
+
}
|
|
6345
|
+
return { ...item };
|
|
6346
|
+
});
|
|
6347
|
+
else if (typeof v === "object" && v !== null && !Array.isArray(v) && relation) flat[k] = { ...v };
|
|
6348
|
+
else flat[k] = v;
|
|
6349
|
+
}
|
|
6350
|
+
return flat;
|
|
6351
|
+
}
|
|
6352
|
+
/**
|
|
6656
6353
|
* Build db.query-compatible options from standard fetch options.
|
|
6657
6354
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
6658
6355
|
*/
|
|
@@ -6722,7 +6419,7 @@ var FetchService = class {
|
|
|
6722
6419
|
async fetchOne(collectionPath, id, databaseId) {
|
|
6723
6420
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6724
6421
|
const table = getTableForCollection(collection, this.registry);
|
|
6725
|
-
const idInfoArray =
|
|
6422
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6726
6423
|
const idInfo = idInfoArray[0];
|
|
6727
6424
|
const idField = table[idInfo.fieldName];
|
|
6728
6425
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6736,7 +6433,7 @@ var FetchService = class {
|
|
|
6736
6433
|
with: withConfig
|
|
6737
6434
|
});
|
|
6738
6435
|
if (!row) return void 0;
|
|
6739
|
-
const flatRow =
|
|
6436
|
+
const flatRow = this.drizzleResultToRow(row, collection);
|
|
6740
6437
|
await this.resolveJoinPathRelations(flatRow, collection, collectionPath, parsedId, databaseId);
|
|
6741
6438
|
return flatRow;
|
|
6742
6439
|
} catch (e) {
|
|
@@ -6778,7 +6475,7 @@ var FetchService = class {
|
|
|
6778
6475
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
6779
6476
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6780
6477
|
const table = getTableForCollection(collection, this.registry);
|
|
6781
|
-
const idInfoArray =
|
|
6478
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6782
6479
|
const idInfo = idInfoArray[0];
|
|
6783
6480
|
const idField = table[idInfo.fieldName];
|
|
6784
6481
|
if (!idField) throw new Error(`ID field '${idInfo.fieldName}' not found in table for collection '${collectionPath}'`);
|
|
@@ -6788,7 +6485,7 @@ var FetchService = class {
|
|
|
6788
6485
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
6789
6486
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
6790
6487
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
6791
|
-
return (await qb.findMany(queryOpts)).map((row) =>
|
|
6488
|
+
return (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRow(row, collection));
|
|
6792
6489
|
} catch (e) {
|
|
6793
6490
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
6794
6491
|
logger.error(`[FetchService] Relation inference error for collection '${collectionPath}': ${e.message}`);
|
|
@@ -6849,10 +6546,8 @@ var FetchService = class {
|
|
|
6849
6546
|
}
|
|
6850
6547
|
/**
|
|
6851
6548
|
* Fallback path used when db.query is unavailable.
|
|
6852
|
-
*
|
|
6853
|
-
*
|
|
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.
|
|
6549
|
+
* The primary path uses drizzleResultToRow which handles relation
|
|
6550
|
+
* mapping without N+1 queries.
|
|
6856
6551
|
*
|
|
6857
6552
|
* Process raw database results into flat rows with relations.
|
|
6858
6553
|
*/
|
|
@@ -6932,7 +6627,10 @@ var FetchService = class {
|
|
|
6932
6627
|
const relationKey = pathSegments[i];
|
|
6933
6628
|
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
6934
6629
|
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
6935
|
-
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6630
|
+
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({
|
|
6631
|
+
...row.values,
|
|
6632
|
+
id: row.id
|
|
6633
|
+
}));
|
|
6936
6634
|
if (i + 1 < pathSegments.length) {
|
|
6937
6635
|
const nextEntityId = pathSegments[i + 1];
|
|
6938
6636
|
currentCollection = relation.target();
|
|
@@ -6994,7 +6692,7 @@ var FetchService = class {
|
|
|
6994
6692
|
if (value === void 0 || value === null) return true;
|
|
6995
6693
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
6996
6694
|
const table = getTableForCollection(collection, this.registry);
|
|
6997
|
-
const idInfoArray =
|
|
6695
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
6998
6696
|
const idInfo = idInfoArray[0];
|
|
6999
6697
|
const idField = table[idInfo.fieldName];
|
|
7000
6698
|
const field = table[fieldName];
|
|
@@ -7021,7 +6719,7 @@ var FetchService = class {
|
|
|
7021
6719
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
7022
6720
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7023
6721
|
const table = getTableForCollection(collection, this.registry);
|
|
7024
|
-
const idInfoArray =
|
|
6722
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7025
6723
|
const idInfo = idInfoArray[0];
|
|
7026
6724
|
const idField = table[idInfo.fieldName];
|
|
7027
6725
|
const tableName = getTableName(table);
|
|
@@ -7029,7 +6727,7 @@ var FetchService = class {
|
|
|
7029
6727
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
7030
6728
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
7031
6729
|
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
7032
|
-
const restRows = (await qb.findMany(queryOpts)).map((row) =>
|
|
6730
|
+
const restRows = (await qb.findMany(queryOpts)).map((row) => this.drizzleResultToRestRow(row, collection));
|
|
7033
6731
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
7034
6732
|
return restRows;
|
|
7035
6733
|
} catch (e) {
|
|
@@ -7078,7 +6776,7 @@ var FetchService = class {
|
|
|
7078
6776
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
7079
6777
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7080
6778
|
const table = getTableForCollection(collection, this.registry);
|
|
7081
|
-
const idInfoArray =
|
|
6779
|
+
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
7082
6780
|
const idInfo = idInfoArray[0];
|
|
7083
6781
|
const idField = table[idInfo.fieldName];
|
|
7084
6782
|
const parsedId = parseIdValues(id, idInfoArray)[idInfo.fieldName];
|
|
@@ -7091,7 +6789,7 @@ var FetchService = class {
|
|
|
7091
6789
|
...withConfig ? { with: withConfig } : {}
|
|
7092
6790
|
});
|
|
7093
6791
|
if (!row) return null;
|
|
7094
|
-
const restRow =
|
|
6792
|
+
const restRow = this.drizzleResultToRestRow(row, collection);
|
|
7095
6793
|
await this.resolveJoinPathRelationsBatchRest([restRow], collection, collectionPath, idInfoArray, include);
|
|
7096
6794
|
return restRow;
|
|
7097
6795
|
} catch (e) {
|
|
@@ -7136,7 +6834,7 @@ var FetchService = class {
|
|
|
7136
6834
|
async fetchRowsWithConditionsRaw(collectionPath, options = {}) {
|
|
7137
6835
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
7138
6836
|
const table = getTableForCollection(collection, this.registry);
|
|
7139
|
-
const idField = table[
|
|
6837
|
+
const idField = table[getPrimaryKeys(collection, this.registry)[0].fieldName];
|
|
7140
6838
|
let vectorMeta;
|
|
7141
6839
|
if (options.vectorSearch) vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
|
|
7142
6840
|
let query = vectorMeta ? this.db.select({
|
|
@@ -8617,8 +8315,8 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8617
8315
|
context: contextForCallback
|
|
8618
8316
|
});
|
|
8619
8317
|
}
|
|
8620
|
-
const savedId =
|
|
8621
|
-
const savedValues = savedRow;
|
|
8318
|
+
const savedId = savedRow.id;
|
|
8319
|
+
const { id: _savedId, ...savedValues } = savedRow;
|
|
8622
8320
|
if (globalCallbacks?.afterSave || callbacks?.afterSave || propertyCallbacks?.afterSave) {
|
|
8623
8321
|
if (globalCallbacks?.afterSave) await globalCallbacks.afterSave({
|
|
8624
8322
|
collection: resolvedCollection,
|
|
@@ -8650,7 +8348,7 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8650
8348
|
}
|
|
8651
8349
|
if (this.historyService && resolvedCollection?.history) this.historyService.recordHistory({
|
|
8652
8350
|
tableName: path,
|
|
8653
|
-
id: savedId,
|
|
8351
|
+
id: savedId.toString(),
|
|
8654
8352
|
action: status === "new" ? "create" : "update",
|
|
8655
8353
|
values: savedValues,
|
|
8656
8354
|
previousValues: previousValuesForHistory,
|
|
@@ -8658,11 +8356,11 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8658
8356
|
});
|
|
8659
8357
|
if (this._deferNotifications) this._pendingNotifications.push({
|
|
8660
8358
|
path,
|
|
8661
|
-
id: savedId,
|
|
8359
|
+
id: savedId.toString(),
|
|
8662
8360
|
row: savedRow,
|
|
8663
8361
|
databaseId: resolvedCollection?.databaseId
|
|
8664
8362
|
});
|
|
8665
|
-
else await this.realtimeService.notifyUpdate(path, savedId, savedRow, resolvedCollection?.databaseId);
|
|
8363
|
+
else await this.realtimeService.notifyUpdate(path, savedId.toString(), savedRow, resolvedCollection?.databaseId);
|
|
8666
8364
|
return savedRow;
|
|
8667
8365
|
} catch (error) {
|
|
8668
8366
|
if (globalCallbacks?.afterSaveError || callbacks?.afterSaveError || propertyCallbacks?.afterSaveError) {
|
|
@@ -8742,7 +8440,10 @@ var PostgresBackendDriver = class PostgresBackendDriver {
|
|
|
8742
8440
|
}
|
|
8743
8441
|
async delete({ row, collection }) {
|
|
8744
8442
|
const targetPath = row.path;
|
|
8745
|
-
const targetRow = {
|
|
8443
|
+
const targetRow = {
|
|
8444
|
+
id: row.id,
|
|
8445
|
+
...row.values ?? {}
|
|
8446
|
+
};
|
|
8746
8447
|
const { collection: resolvedCollection, callbacks, globalCallbacks, propertyCallbacks } = this.resolveCollectionCallbacks(collection, targetPath);
|
|
8747
8448
|
const contextForCallback = this.buildCallContext();
|
|
8748
8449
|
if (globalCallbacks?.beforeDelete || callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
|
|
@@ -9690,7 +9391,6 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
9690
9391
|
});
|
|
9691
9392
|
});
|
|
9692
9393
|
schemaContent += "\n";
|
|
9693
|
-
const junctionSpecs = resolveJunctionSpecs(collections);
|
|
9694
9394
|
for (const collection of collections) {
|
|
9695
9395
|
const tableName = getTableName$1(collection);
|
|
9696
9396
|
if (tableName) allTablesToGenerate.set(tableName, { collection });
|
|
@@ -9724,17 +9424,9 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
9724
9424
|
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
9725
9425
|
schemaContent += ` ${sourceColumn}: ${sourceColType}(\"${sourceColumn}\").notNull().references(() => ${getTableVarName(getTableName$1(sourceCollection))}.${sourceId}, ${refOptions}),\n`;
|
|
9726
9426
|
schemaContent += ` ${targetColumn}: ${targetColType}(\"${targetColumn}\").notNull().references(() => ${getTableVarName(getTableName$1(targetCollection))}.${targetId}, ${refOptions}),\n`;
|
|
9727
|
-
schemaContent += "}, (table) => (
|
|
9728
|
-
schemaContent += ` primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })
|
|
9729
|
-
|
|
9730
|
-
if (!stripPolicies && junctionSpec) {
|
|
9731
|
-
const junctionCollection = getJunctionCollectionConfig(junctionSpec);
|
|
9732
|
-
const resolveCollection = (slug) => collections.find((c) => c.slug === slug || getTableName$1(c) === slug);
|
|
9733
|
-
getJunctionSecurityRules(junctionSpec).forEach((rule, idx) => {
|
|
9734
|
-
schemaContent += generatePolicyCode(junctionCollection, rule, idx, resolveCollection);
|
|
9735
|
-
});
|
|
9736
|
-
}
|
|
9737
|
-
schemaContent += "])).enableRLS();\n\n";
|
|
9427
|
+
schemaContent += "}, (table) => ({\n";
|
|
9428
|
+
schemaContent += ` pk: primaryKey({ columns: [table.${sourceColumn}, table.${targetColumn}] })\n`;
|
|
9429
|
+
schemaContent += "}));\n\n";
|
|
9738
9430
|
} else if (!isJunction) {
|
|
9739
9431
|
const schema = isPostgresCollectionConfig(collection) ? collection.schema : void 0;
|
|
9740
9432
|
const tableCreator = schema ? `${schema}Schema.table` : "pgTable";
|
|
@@ -10425,7 +10117,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10425
10117
|
startAfter: request.startAfter,
|
|
10426
10118
|
searchString: request.searchString
|
|
10427
10119
|
}, authContext);
|
|
10428
|
-
this.sendCollectionUpdate(clientId, subscriptionId, rows
|
|
10120
|
+
this.sendCollectionUpdate(clientId, subscriptionId, rows);
|
|
10429
10121
|
} catch (error) {
|
|
10430
10122
|
const sanitized = sanitizeErrorForClient(error, request.path);
|
|
10431
10123
|
this.sendError(clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10526,7 +10218,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10526
10218
|
if (subscription.type === "single" && notifyPath === originalPath) if (row && row?._rebase_invalidated) this.debouncedSingleRefetch(subscriptionId, notifyPath, id, subscription);
|
|
10527
10219
|
else this.sendSingleUpdate(subscription.clientId, subscriptionId, row);
|
|
10528
10220
|
else if (subscription.type === "collection" && subscription.collectionRequest) {
|
|
10529
|
-
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row
|
|
10221
|
+
if (!row || !row?._rebase_invalidated) this.sendCollectionPatch(subscription.clientId, subscriptionId, id, row);
|
|
10530
10222
|
this.debouncedCollectionRefetch(subscriptionId, notifyPath, subscription);
|
|
10531
10223
|
}
|
|
10532
10224
|
} catch (error) {
|
|
@@ -10556,7 +10248,7 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10556
10248
|
if (!this._subscriptions.has(subscriptionId)) return;
|
|
10557
10249
|
try {
|
|
10558
10250
|
const rows = await this.fetchCollectionWithAuth(notifyPath, subscription.collectionRequest, subscription.authContext);
|
|
10559
|
-
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows
|
|
10251
|
+
this.sendCollectionUpdate(subscription.clientId, subscriptionId, rows);
|
|
10560
10252
|
} catch (error) {
|
|
10561
10253
|
const sanitized = sanitizeErrorForClient(error, notifyPath);
|
|
10562
10254
|
this.sendError(subscription.clientId, sanitized.message, subscriptionId, sanitized.code);
|
|
@@ -10770,12 +10462,11 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10770
10462
|
}
|
|
10771
10463
|
return await this.dataService.fetchOne(notifyPath, id);
|
|
10772
10464
|
}
|
|
10773
|
-
sendCollectionUpdate(clientId, subscriptionId, rows
|
|
10465
|
+
sendCollectionUpdate(clientId, subscriptionId, rows) {
|
|
10774
10466
|
const message = {
|
|
10775
10467
|
type: "collection_update",
|
|
10776
10468
|
subscriptionId,
|
|
10777
|
-
rows
|
|
10778
|
-
pks: this.primaryKeysForPath(path)
|
|
10469
|
+
rows
|
|
10779
10470
|
};
|
|
10780
10471
|
this.sendMessage(clientId, message);
|
|
10781
10472
|
}
|
|
@@ -10790,33 +10481,16 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
10790
10481
|
/**
|
|
10791
10482
|
* Send a lightweight row-level patch to a collection subscriber.
|
|
10792
10483
|
* The client can merge this into its cached data for instant feedback.
|
|
10793
|
-
*
|
|
10794
|
-
* The key columns ride along: the patch names a row by address, and the
|
|
10795
|
-
* client has to find that row among the ones it cached — which carry
|
|
10796
|
-
* columns and no address. The SDK holds no collection config to derive one
|
|
10797
|
-
* from, so this is the only place the mapping can come from.
|
|
10798
10484
|
*/
|
|
10799
|
-
sendCollectionPatch(clientId, subscriptionId, id, row
|
|
10485
|
+
sendCollectionPatch(clientId, subscriptionId, id, row) {
|
|
10800
10486
|
const message = {
|
|
10801
10487
|
type: "collection_patch",
|
|
10802
10488
|
subscriptionId,
|
|
10803
10489
|
id,
|
|
10804
|
-
row
|
|
10805
|
-
pks: this.primaryKeysForPath(notifyPath)
|
|
10490
|
+
row
|
|
10806
10491
|
};
|
|
10807
10492
|
this.sendMessage(clientId, message);
|
|
10808
10493
|
}
|
|
10809
|
-
/** The key columns of the collection at `path`, if they can be resolved. */
|
|
10810
|
-
primaryKeysForPath(path) {
|
|
10811
|
-
try {
|
|
10812
|
-
const collection = this.registry.getCollectionByPath(path);
|
|
10813
|
-
if (!collection) return void 0;
|
|
10814
|
-
const keys = getPrimaryKeys(collection, this.registry);
|
|
10815
|
-
return keys.length > 0 ? keys : void 0;
|
|
10816
|
-
} catch {
|
|
10817
|
-
return;
|
|
10818
|
-
}
|
|
10819
|
-
}
|
|
10820
10494
|
sendError(clientId, error, subscriptionId, code) {
|
|
10821
10495
|
const message = {
|
|
10822
10496
|
type: "error",
|
|
@@ -11064,7 +10738,12 @@ var RealtimeService = class RealtimeService extends EventEmitter {
|
|
|
11064
10738
|
}
|
|
11065
10739
|
/** Compute the canonical (possibly composite) id string from a captured row. */
|
|
11066
10740
|
extractIdFromCdcRow(collection, row) {
|
|
11067
|
-
|
|
10741
|
+
try {
|
|
10742
|
+
const composite = buildCompositeId(row, getPrimaryKeys(collection, this.registry));
|
|
10743
|
+
if (composite && composite !== ":::") return composite;
|
|
10744
|
+
} catch {}
|
|
10745
|
+
if (row.id !== void 0 && row.id !== null) return String(row.id);
|
|
10746
|
+
return "*";
|
|
11068
10747
|
}
|
|
11069
10748
|
dedupKey(path, id, databaseId) {
|
|
11070
10749
|
return `${databaseId ?? ""}::${path}::${id}`;
|
|
@@ -20524,33 +20203,6 @@ function formatBytes(bytes) {
|
|
|
20524
20203
|
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
20525
20204
|
}
|
|
20526
20205
|
//#endregion
|
|
20527
|
-
//#region src/collections/buildRegistry.ts
|
|
20528
|
-
/**
|
|
20529
|
-
* Build the collection registry for a driver.
|
|
20530
|
-
*
|
|
20531
|
-
* The order matters and is the reason this is one function rather than a run of
|
|
20532
|
-
* statements in the bootstrapper. Keys are resolved from the drizzle schema, so
|
|
20533
|
-
* anything that inspects them has to run *after* the tables are registered —
|
|
20534
|
-
* and `warnOnKeysTheAdminCannotResolve` fails open if it does not, because a
|
|
20535
|
-
* collection whose table it cannot look up is one it has nothing to say about.
|
|
20536
|
-
* Warned too early, it would skip every collection and report nothing, which
|
|
20537
|
-
* reads exactly like having nothing to report.
|
|
20538
|
-
*/
|
|
20539
|
-
function buildCollectionRegistry(schema) {
|
|
20540
|
-
const registry = new PostgresCollectionRegistry();
|
|
20541
|
-
if (schema.collections) {
|
|
20542
|
-
registry.registerMultiple(schema.collections);
|
|
20543
|
-
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
20544
|
-
}
|
|
20545
|
-
if (schema.tables) Object.values(schema.tables).forEach((table) => {
|
|
20546
|
-
if (isTable(table)) registry.registerTable(table, getTableName(table));
|
|
20547
|
-
});
|
|
20548
|
-
if (schema.enums) registry.registerEnums(schema.enums);
|
|
20549
|
-
if (schema.relations) registry.registerRelations(schema.relations);
|
|
20550
|
-
warnOnKeysTheAdminCannotResolve(registry.getCollections(), registry);
|
|
20551
|
-
return registry;
|
|
20552
|
-
}
|
|
20553
|
-
//#endregion
|
|
20554
20206
|
//#region src/auth/ensure-tables.ts
|
|
20555
20207
|
/**
|
|
20556
20208
|
* Auto-create auth tables if they don't exist.
|
|
@@ -22533,14 +22185,21 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
22533
22185
|
logger.info(`🔍 [PostgresRegistry] BaaS mode: derived ${introspectedCollections.length} collections from schema "${pgSchemaName}" [${introspectedCollections.map((c) => c.slug).join(", ")}]`);
|
|
22534
22186
|
}
|
|
22535
22187
|
const activeCollections = introspectedCollections ?? collections;
|
|
22188
|
+
const registry = new PostgresCollectionRegistry();
|
|
22189
|
+
if (activeCollections) {
|
|
22190
|
+
registry.registerMultiple(activeCollections);
|
|
22191
|
+
logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
|
|
22192
|
+
}
|
|
22536
22193
|
const schemaTables = introspectedTables ?? pgConfig.schema?.tables;
|
|
22537
|
-
|
|
22538
|
-
|
|
22539
|
-
|
|
22540
|
-
|
|
22541
|
-
|
|
22542
|
-
relations: schemaRelations
|
|
22194
|
+
if (schemaTables) Object.values(schemaTables).forEach((table) => {
|
|
22195
|
+
if (isTable(table)) {
|
|
22196
|
+
const tableName = getTableName(table);
|
|
22197
|
+
registry.registerTable(table, tableName);
|
|
22198
|
+
}
|
|
22543
22199
|
});
|
|
22200
|
+
if (pgConfig.schema?.enums) registry.registerEnums(pgConfig.schema.enums);
|
|
22201
|
+
const schemaRelations = introspectedRelations ?? pgConfig.schema?.relations;
|
|
22202
|
+
if (schemaRelations) registry.registerRelations(schemaRelations);
|
|
22544
22203
|
if (schemaTables) patchPgArrayNullSafety(schemaTables);
|
|
22545
22204
|
const mergedSchema = {
|
|
22546
22205
|
...schemaTables,
|