1
13
14export type Cardinality = "ONE_TO_ONE" | "ONE_TO_MANY" | "MANY_TO_MANY";
15
16export interface LinkDiff {
17 added: string[];
18 removed: string[];
19 unchanged: string[];
20}
21
22
27export function diffLinks(current: readonly string[], desired: readonly string[]): LinkDiff {
28 const cur = new Set(current);
29 const des = new Set(desired);
30 const added: string[] = [];
31 const removed: string[] = [];
32 const unchanged: string[] = [];
33 for (const id of desired) {
34 if (cur.has(id)) unchanged.push(id);
35 else added.push(id);
36 }
37 for (const id of current) {
38 if (!des.has(id)) removed.push(id);
39 }
40 return { added, removed, unchanged };
41}
42
43export interface CardinalityCheck {
44 ok: boolean;
45
46 replaced: string[];
47 error?: string;
48}
49
50
62export function applyCardinality(
63 current: readonly string[],
64 desired: readonly string[],
65 cardinality: Cardinality,
66 isOneSide: boolean,
67): CardinalityCheck {
68 const deduped = [...new Set(desired)];
69 if (cardinality === "MANY_TO_MANY") return { ok: true, replaced: [] };
70 if (isOneSide) {
71 if (deduped.length > 1) {
72 return { ok: false, replaced: [], error: "one_side_exceeds_one_target" };
73 }
74
75 const replaced = current.filter((id) => !deduped.includes(id));
76 return { ok: true, replaced };
77 }
78
79 return { ok: true, replaced: [] };
80}
81
82
88export interface SetLinksPlan {
89 deleteIds: string[];
90 insertIds: string[];
91
92 displacedPartners: Array<{ targetId: string; partnerRecordId: string }>;
93 replaced: string[];
94}
95
96export function planSetLinks(
97 current: readonly string[],
98 desired: readonly string[],
99 cardinality: Cardinality,
100 isOneSide: boolean,
101 targetPartners: ReadonlyMap<string, string>,
102): SetLinksPlan {
103 const check = applyCardinality(current, desired, cardinality, isOneSide);
104 if (!check.ok) throw new Error(check.error ?? "cardinality violation");
105 const { added, removed } = diffLinks(current, desired);
106 const deduped = [...new Set(desired)];
107 const displacedPartners: Array<{ targetId: string; partnerRecordId: string }> = [];
108 if (cardinality === "ONE_TO_ONE") {
109 for (const targetId of deduped) {
110 const partner = targetPartners.get(targetId);
111 if (partner !== undefined) displacedPartners.push({ targetId, partnerRecordId: partner });
112 }
113 }
114 return { deleteIds: removed, insertIds: added, displacedPartners, replaced: check.replaced };
115}
116
117
118export function linksForRecord(
119 links: readonly {
120 databaseId: string;
121 recordId: string;
122 targetDatabaseId: string;
123 targetRecordId: string;
124 }[],
125 recordId: string,
126): string[] {
127 const out = new Set<string>();
128 for (const l of links) {
129 if (l.recordId === recordId) out.add(l.targetRecordId);
130 }
131 return [...out];
132}
133
134
135export function relationCounts(
136 links: readonly { recordId: string; targetRecordId: string }[],
137 recordIds: readonly string[],
138): Record<string, number> {
139 const counts: Record<string, number> = {};
140 for (const id of recordIds) counts[id] = 0;
141 for (const l of links) {
142 if (counts[l.recordId] !== undefined) counts[l.recordId] = (counts[l.recordId] ?? 0) + 1;
143 }
144 return counts;
145}
146