clivly 0.13.0 → 0.14.1
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/README.md +10 -3
- package/{adapter-DhPBRNpi.d.mts → adapter-B-iBMhl0.d.mts} +13 -0
- package/{adapter-DTMeGGsl.d.cts → adapter-JpXSAnj-.d.cts} +13 -0
- package/core-adapter.d.cts +1 -1
- package/core-adapter.d.mts +1 -1
- package/core-sync-engine.cjs +3 -1
- package/core-sync-engine.d.cts +2 -2
- package/core-sync-engine.d.mts +2 -2
- package/core-sync-engine.mjs +2 -2
- package/core.cjs +3 -1
- package/core.d.cts +3 -3
- package/core.d.mts +3 -3
- package/core.mjs +2 -2
- package/index.cjs +102 -1
- package/index.mjs +102 -1
- package/internal-adapter.d.cts +1 -1
- package/internal-adapter.d.mts +1 -1
- package/internal-sync-engine.cjs +3 -1
- package/internal-sync-engine.d.cts +2 -2
- package/internal-sync-engine.d.mts +2 -2
- package/internal-sync-engine.mjs +2 -2
- package/internal.cjs +3 -1
- package/internal.d.cts +3 -3
- package/internal.d.mts +3 -3
- package/internal.mjs +2 -2
- package/package.json +1 -1
- package/{sdk-B-dp1tZN.mjs → sdk-DLoVPlGB.mjs} +139 -5
- package/{sdk-DKfKhJD_.cjs → sdk-ZMy4C4VK.cjs} +156 -4
- package/sdk.cjs +4 -1
- package/sdk.d.cts +32 -0
- package/sdk.d.mts +32 -0
- package/sdk.mjs +2 -2
- package/{sync-engine-Dwm_KDUS.cjs → sync-engine-BGVDsrOQ.cjs} +131 -71
- package/{sync-engine-V3_UutGv.d.cts → sync-engine-CYsWOujz.d.cts} +63 -22
- package/{sync-engine--M9EaONC.d.mts → sync-engine-C_5TpvfX.d.mts} +63 -22
- package/{sync-engine-Df2MCs_n.mjs → sync-engine-DH058G3w.mjs} +120 -72
|
@@ -1,67 +1,103 @@
|
|
|
1
1
|
const require_view_compiler = require("./view-compiler-xiwm5iar.cjs");
|
|
2
2
|
//#region ../core/src/sync-engine.ts
|
|
3
|
+
/** Actions that remove something — only safe when the source set is complete. */
|
|
4
|
+
function isRemovalAction(action) {
|
|
5
|
+
return action.kind === "archive" || action.kind === "endMembership";
|
|
6
|
+
}
|
|
7
|
+
/** Every distinct non-null company ref in a person's rows, in arrival order. */
|
|
8
|
+
function collectCompanyRefs(rows) {
|
|
9
|
+
const refs = [];
|
|
10
|
+
for (const row of rows) {
|
|
11
|
+
const ref = row.companyRef ?? null;
|
|
12
|
+
if (ref !== null && !refs.includes(ref)) refs.push(ref);
|
|
13
|
+
}
|
|
14
|
+
return refs;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The primary company for a person: the one they already have while it is
|
|
18
|
+
* still one of theirs — so the displayed organisation never flips between
|
|
19
|
+
* syncs — otherwise the lowest ref, which is deterministic for a new person.
|
|
20
|
+
*/
|
|
21
|
+
function pickPrimaryRef(refs, current) {
|
|
22
|
+
if (current !== null && refs.includes(current)) return current;
|
|
23
|
+
return [...refs].sort()[0] ?? null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The mirror row that represents a source identity. Normally there is exactly
|
|
27
|
+
* one; before contact consolidation has run there can be several (one per
|
|
28
|
+
* tenant), and the live, earliest-listed one stands for the person while the
|
|
29
|
+
* rest are left alone for the consolidation script to fold in.
|
|
30
|
+
*/
|
|
31
|
+
function canonicalMirror(rows) {
|
|
32
|
+
return rows.find((row) => !row.archivedAt) ?? rows[0];
|
|
33
|
+
}
|
|
3
34
|
/**
|
|
4
35
|
* Diff a view's rows against the current mirror population for one entity.
|
|
5
36
|
*
|
|
6
|
-
* - source
|
|
7
|
-
* - source
|
|
8
|
-
* - source
|
|
9
|
-
* - live mirror row absent from source
|
|
10
|
-
* - archived mirror row absent from source
|
|
37
|
+
* - source identity with no mirror row → create
|
|
38
|
+
* - source identity with a live mirror row → update (overwrite identity fields)
|
|
39
|
+
* - source identity with an archived mirror → restore (re-activate + update)
|
|
40
|
+
* - live mirror row absent from source → archive (soft delete, keep history)
|
|
41
|
+
* - archived mirror row absent from source → no-op
|
|
11
42
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
43
|
+
* Identity is always the source primary key. A `through` (join-table) view
|
|
44
|
+
* fans one person out into a row per tenant; those rows are ONE person, so they
|
|
45
|
+
* collapse into one action. With `memberships` on (contacts), that action also
|
|
46
|
+
* carries every tenant ref the person was seen with, and each tenant the mirror
|
|
47
|
+
* still has as active but the source no longer lists yields `endMembership` —
|
|
48
|
+
* a person leaving one tenant ends that membership and keeps the others.
|
|
18
49
|
*/
|
|
19
50
|
function reconcile(source, mirror, opts = {}) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const rows = [];
|
|
23
|
-
for (const row of source) {
|
|
24
|
-
const key = keyOf(row.sourceRef, row.companyRef ?? null);
|
|
25
|
-
if (!seen.has(key)) {
|
|
26
|
-
seen.add(key);
|
|
27
|
-
rows.push(row);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
const byKey = new Map(mirror.map((m) => [keyOf(m.sourceRef, m.companyRef), m]));
|
|
31
|
-
const liveKeys = /* @__PURE__ */ new Set();
|
|
51
|
+
const rowsByRef = groupBySourceRef(source);
|
|
52
|
+
const mirrorByRef = groupBySourceRef(mirror);
|
|
32
53
|
const actions = [];
|
|
33
|
-
for (const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
});
|
|
51
|
-
else actions.push({
|
|
52
|
-
kind: "update",
|
|
53
|
-
id: existing.id,
|
|
54
|
-
sourceRef: row.sourceRef,
|
|
55
|
-
fields: row.fields,
|
|
56
|
-
companyRef
|
|
57
|
-
});
|
|
54
|
+
for (const [sourceRef, rows] of rowsByRef) {
|
|
55
|
+
const existing = canonicalMirror(mirrorByRef.get(sourceRef) ?? []);
|
|
56
|
+
actions.push(...personActions(sourceRef, rows, existing, opts));
|
|
57
|
+
}
|
|
58
|
+
for (const m of mirror) if (!(rowsByRef.has(m.sourceRef) || m.archivedAt)) actions.push({
|
|
59
|
+
kind: "archive",
|
|
60
|
+
id: m.id,
|
|
61
|
+
sourceRef: m.sourceRef
|
|
62
|
+
});
|
|
63
|
+
return actions;
|
|
64
|
+
}
|
|
65
|
+
function groupBySourceRef(items) {
|
|
66
|
+
const out = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const item of items) {
|
|
68
|
+
const list = out.get(item.sourceRef);
|
|
69
|
+
if (list) list.push(item);
|
|
70
|
+
else out.set(item.sourceRef, [item]);
|
|
58
71
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/** The create/update/restore for one present person, plus any ended tenants. */
|
|
75
|
+
function personActions(sourceRef, rows, existing, opts) {
|
|
76
|
+
const first = rows[0];
|
|
77
|
+
const refs = collectCompanyRefs(rows);
|
|
78
|
+
const companyRef = opts.memberships ? pickPrimaryRef(refs, existing?.companyRef ?? null) : first.companyRef ?? null;
|
|
79
|
+
const membershipSet = opts.memberships ? { companyRefs: refs } : {};
|
|
80
|
+
if (!existing) return [{
|
|
81
|
+
kind: "create",
|
|
82
|
+
sourceRef,
|
|
83
|
+
fields: first.fields,
|
|
84
|
+
companyRef,
|
|
85
|
+
...membershipSet
|
|
86
|
+
}];
|
|
87
|
+
const actions = [{
|
|
88
|
+
kind: existing.archivedAt ? "restore" : "update",
|
|
89
|
+
id: existing.id,
|
|
90
|
+
sourceRef,
|
|
91
|
+
fields: first.fields,
|
|
92
|
+
companyRef,
|
|
93
|
+
...membershipSet
|
|
94
|
+
}];
|
|
95
|
+
if (opts.memberships) {
|
|
96
|
+
for (const held of existing.companyRefs ?? []) if (!refs.includes(held)) actions.push({
|
|
97
|
+
kind: "endMembership",
|
|
98
|
+
id: existing.id,
|
|
99
|
+
sourceRef,
|
|
100
|
+
companyRef: held
|
|
65
101
|
});
|
|
66
102
|
}
|
|
67
103
|
return actions;
|
|
@@ -74,20 +110,40 @@ const CONCEPT_ORDER = {
|
|
|
74
110
|
function syncOrder(a, b) {
|
|
75
111
|
return (CONCEPT_ORDER[a.concept] ?? 99) - (CONCEPT_ORDER[b.concept] ?? 99);
|
|
76
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve each action's host-side company refs — the primary one and, for
|
|
115
|
+
* contacts, every membership — to `crm_companies.id`s in one lookup. Shared by
|
|
116
|
+
* `runSync` and the cloud push receiver so both persist identical actions.
|
|
117
|
+
*/
|
|
77
118
|
async function resolveActions(store, actions) {
|
|
78
119
|
const refs = /* @__PURE__ */ new Set();
|
|
79
|
-
for (const action of actions)
|
|
120
|
+
for (const action of actions) {
|
|
121
|
+
if (isRemovalAction(action) || !("fields" in action)) continue;
|
|
122
|
+
if (action.companyRef) refs.add(action.companyRef);
|
|
123
|
+
for (const ref of action.companyRefs ?? []) refs.add(ref);
|
|
124
|
+
}
|
|
80
125
|
const resolved = refs.size ? await store.resolveCompanyRefs([...refs]) : /* @__PURE__ */ new Map();
|
|
81
126
|
return actions.map((action) => {
|
|
82
|
-
if (action.kind === "archive") return action;
|
|
127
|
+
if (action.kind === "archive" || action.kind === "endMembership") return action;
|
|
83
128
|
const companyId = action.companyRef ? resolved.get(action.companyRef) ?? null : null;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
129
|
+
const memberships = action.companyRefs?.map((companyRef) => ({
|
|
130
|
+
companyRef,
|
|
131
|
+
companyId: resolved.get(companyRef) ?? null
|
|
132
|
+
}));
|
|
133
|
+
const base = {
|
|
87
134
|
sourceRef: action.sourceRef,
|
|
88
135
|
fields: action.fields,
|
|
89
136
|
companyId,
|
|
90
|
-
companySourceRef: action.companyRef ?? null
|
|
137
|
+
companySourceRef: action.companyRef ?? null,
|
|
138
|
+
...memberships ? { memberships } : {}
|
|
139
|
+
};
|
|
140
|
+
return action.kind === "create" ? {
|
|
141
|
+
kind: "create",
|
|
142
|
+
...base
|
|
143
|
+
} : {
|
|
144
|
+
kind: action.kind,
|
|
145
|
+
id: action.id,
|
|
146
|
+
...base
|
|
91
147
|
};
|
|
92
148
|
});
|
|
93
149
|
}
|
|
@@ -96,19 +152,11 @@ function emptyTally() {
|
|
|
96
152
|
create: 0,
|
|
97
153
|
update: 0,
|
|
98
154
|
restore: 0,
|
|
99
|
-
archive: 0
|
|
155
|
+
archive: 0,
|
|
156
|
+
endMembership: 0
|
|
100
157
|
};
|
|
101
158
|
}
|
|
102
159
|
/**
|
|
103
|
-
* An entity whose company relationship is a `through` (join-table) join fans out
|
|
104
|
-
* into one source row per membership, so its mirror must be keyed compositely.
|
|
105
|
-
*/
|
|
106
|
-
function isCompositeEntity(config, entityKey) {
|
|
107
|
-
const entity = config.entities[entityKey];
|
|
108
|
-
if (!entity?.relationships) return false;
|
|
109
|
-
return Object.values(entity.relationships).some((rel) => "through" in rel.via);
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
160
|
* Run a full sync: compile the config's views, then reconcile + persist each,
|
|
113
161
|
* companies first. Returns per-entity and total action counts.
|
|
114
162
|
*/
|
|
@@ -129,7 +177,7 @@ async function runSync(config, store, options = {}) {
|
|
|
129
177
|
role: view.role,
|
|
130
178
|
objectTypeId
|
|
131
179
|
})]);
|
|
132
|
-
const actions = reconcile(source, mirror, {
|
|
180
|
+
const actions = reconcile(source, mirror, { memberships: view.concept === "contact" }).filter((a) => !(options.sample && isRemovalAction(a)));
|
|
133
181
|
const persistActions = await resolveActions(store, actions);
|
|
134
182
|
await store.persist({
|
|
135
183
|
concept: view.concept,
|
|
@@ -217,6 +265,12 @@ Object.defineProperty(exports, "computeSampleWarnings", {
|
|
|
217
265
|
return computeSampleWarnings;
|
|
218
266
|
}
|
|
219
267
|
});
|
|
268
|
+
Object.defineProperty(exports, "isRemovalAction", {
|
|
269
|
+
enumerable: true,
|
|
270
|
+
get: function() {
|
|
271
|
+
return isRemovalAction;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
220
274
|
Object.defineProperty(exports, "previewSync", {
|
|
221
275
|
enumerable: true,
|
|
222
276
|
get: function() {
|
|
@@ -229,6 +283,12 @@ Object.defineProperty(exports, "reconcile", {
|
|
|
229
283
|
return reconcile;
|
|
230
284
|
}
|
|
231
285
|
});
|
|
286
|
+
Object.defineProperty(exports, "resolveActions", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
get: function() {
|
|
289
|
+
return resolveActions;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
232
292
|
Object.defineProperty(exports, "runSync", {
|
|
233
293
|
enumerable: true,
|
|
234
294
|
get: function() {
|
|
@@ -30,77 +30,112 @@ interface SourceRow {
|
|
|
30
30
|
/** An existing mirror row, as far as reconciliation needs to know. */
|
|
31
31
|
interface MirrorRecord {
|
|
32
32
|
archivedAt: Date | null;
|
|
33
|
-
/** Host-side
|
|
33
|
+
/** Host-side key of the row's primary company (its `company_source_ref`). */
|
|
34
34
|
companyRef: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Host-side keys of the person's ACTIVE memberships. Only listed for
|
|
37
|
+
* contacts; reconciliation ends any membership absent from a full sync.
|
|
38
|
+
*/
|
|
39
|
+
companyRefs?: string[];
|
|
35
40
|
id: string;
|
|
36
41
|
sourceRef: string;
|
|
37
42
|
}
|
|
38
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The tenants a person belongs to, as seen in this batch. Present on contact
|
|
45
|
+
* actions only (the `memberships` reconcile option); `companyRef` alongside it
|
|
46
|
+
* is the one picked as primary.
|
|
47
|
+
*/
|
|
48
|
+
interface MembershipSet {
|
|
49
|
+
companyRefs?: string[];
|
|
50
|
+
}
|
|
51
|
+
type SyncAction = ({
|
|
39
52
|
kind: "create";
|
|
40
53
|
sourceRef: string;
|
|
41
54
|
fields: Record<string, string | null>;
|
|
42
55
|
companyRef: string | null;
|
|
43
|
-
} | {
|
|
56
|
+
} & MembershipSet) | ({
|
|
44
57
|
kind: "update";
|
|
45
58
|
id: string;
|
|
46
59
|
sourceRef: string;
|
|
47
60
|
fields: Record<string, string | null>;
|
|
48
61
|
companyRef: string | null;
|
|
49
|
-
} | {
|
|
62
|
+
} & MembershipSet) | ({
|
|
50
63
|
kind: "restore";
|
|
51
64
|
id: string;
|
|
52
65
|
sourceRef: string;
|
|
53
66
|
fields: Record<string, string | null>;
|
|
54
67
|
companyRef: string | null;
|
|
55
|
-
} | {
|
|
68
|
+
} & MembershipSet) | {
|
|
56
69
|
kind: "archive";
|
|
57
70
|
id: string;
|
|
58
71
|
sourceRef: string;
|
|
72
|
+
} | {
|
|
73
|
+
kind: "endMembership";
|
|
74
|
+
id: string;
|
|
75
|
+
sourceRef: string;
|
|
76
|
+
companyRef: string;
|
|
59
77
|
};
|
|
78
|
+
/** A membership with its host tenant key resolved to a `crm_companies.id`. */
|
|
79
|
+
interface ResolvedMembership {
|
|
80
|
+
companyId: string | null;
|
|
81
|
+
companyRef: string;
|
|
82
|
+
}
|
|
83
|
+
interface ResolvedMembershipSet {
|
|
84
|
+
memberships?: ResolvedMembership[];
|
|
85
|
+
}
|
|
60
86
|
/** A persist action with the company ref resolved to a `crm_companies.id`. */
|
|
61
|
-
type PersistAction = {
|
|
87
|
+
type PersistAction = ({
|
|
62
88
|
kind: "create";
|
|
63
89
|
sourceRef: string;
|
|
64
90
|
fields: Record<string, string | null>;
|
|
65
91
|
companyId: string | null;
|
|
66
92
|
companySourceRef: string | null;
|
|
67
|
-
} | {
|
|
93
|
+
} & ResolvedMembershipSet) | ({
|
|
68
94
|
kind: "update";
|
|
69
95
|
id: string;
|
|
70
96
|
sourceRef: string;
|
|
71
97
|
fields: Record<string, string | null>;
|
|
72
98
|
companyId: string | null;
|
|
73
99
|
companySourceRef: string | null;
|
|
74
|
-
} | {
|
|
100
|
+
} & ResolvedMembershipSet) | ({
|
|
75
101
|
kind: "restore";
|
|
76
102
|
id: string;
|
|
77
103
|
sourceRef: string;
|
|
78
104
|
fields: Record<string, string | null>;
|
|
79
105
|
companyId: string | null;
|
|
80
106
|
companySourceRef: string | null;
|
|
81
|
-
} | {
|
|
107
|
+
} & ResolvedMembershipSet) | {
|
|
82
108
|
kind: "archive";
|
|
83
109
|
id: string;
|
|
84
110
|
sourceRef: string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "endMembership";
|
|
113
|
+
id: string;
|
|
114
|
+
sourceRef: string;
|
|
115
|
+
companyRef: string;
|
|
85
116
|
};
|
|
117
|
+
/** Actions that remove something — only safe when the source set is complete. */
|
|
118
|
+
declare function isRemovalAction(action: {
|
|
119
|
+
kind: string;
|
|
120
|
+
}): boolean;
|
|
86
121
|
/**
|
|
87
122
|
* Diff a view's rows against the current mirror population for one entity.
|
|
88
123
|
*
|
|
89
|
-
* - source
|
|
90
|
-
* - source
|
|
91
|
-
* - source
|
|
92
|
-
* - live mirror row absent from source
|
|
93
|
-
* - archived mirror row absent from source
|
|
124
|
+
* - source identity with no mirror row → create
|
|
125
|
+
* - source identity with a live mirror row → update (overwrite identity fields)
|
|
126
|
+
* - source identity with an archived mirror → restore (re-activate + update)
|
|
127
|
+
* - live mirror row absent from source → archive (soft delete, keep history)
|
|
128
|
+
* - archived mirror row absent from source → no-op
|
|
94
129
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
130
|
+
* Identity is always the source primary key. A `through` (join-table) view
|
|
131
|
+
* fans one person out into a row per tenant; those rows are ONE person, so they
|
|
132
|
+
* collapse into one action. With `memberships` on (contacts), that action also
|
|
133
|
+
* carries every tenant ref the person was seen with, and each tenant the mirror
|
|
134
|
+
* still has as active but the source no longer lists yields `endMembership` —
|
|
135
|
+
* a person leaving one tenant ends that membership and keeps the others.
|
|
101
136
|
*/
|
|
102
137
|
declare function reconcile(source: SourceRow[], mirror: MirrorRecord[], opts?: {
|
|
103
|
-
|
|
138
|
+
memberships?: boolean;
|
|
104
139
|
}): SyncAction[];
|
|
105
140
|
/** DB I/O port. The concrete store lives in the SDK/drizzle package. */
|
|
106
141
|
interface SyncStore {
|
|
@@ -147,6 +182,12 @@ interface SyncResult {
|
|
|
147
182
|
archived: number;
|
|
148
183
|
};
|
|
149
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolve each action's host-side company refs — the primary one and, for
|
|
187
|
+
* contacts, every membership — to `crm_companies.id`s in one lookup. Shared by
|
|
188
|
+
* `runSync` and the cloud push receiver so both persist identical actions.
|
|
189
|
+
*/
|
|
190
|
+
declare function resolveActions(store: SyncStore, actions: SyncAction[]): Promise<PersistAction[]>;
|
|
150
191
|
/**
|
|
151
192
|
* Run a full sync: compile the config's views, then reconcile + persist each,
|
|
152
193
|
* companies first. Returns per-entity and total action counts.
|
|
@@ -181,4 +222,4 @@ declare function previewSync(store: SyncStore, view: CompiledView, opts: {
|
|
|
181
222
|
sourceTable: string;
|
|
182
223
|
}): Promise<PreviewResult>;
|
|
183
224
|
//#endregion
|
|
184
|
-
export { PreviewWarning as a,
|
|
225
|
+
export { PreviewWarning as a, SyncAction as c, computeSampleWarnings as d, isRemovalAction as f, runSync as g, resolveActions as h, PreviewResult as i, SyncResult as l, reconcile as m, MirrorRecord as n, ResolvedMembership as o, previewSync as p, PersistAction as r, SourceRow as s, EntitySyncResult as t, SyncStore as u };
|
|
@@ -30,77 +30,112 @@ interface SourceRow {
|
|
|
30
30
|
/** An existing mirror row, as far as reconciliation needs to know. */
|
|
31
31
|
interface MirrorRecord {
|
|
32
32
|
archivedAt: Date | null;
|
|
33
|
-
/** Host-side
|
|
33
|
+
/** Host-side key of the row's primary company (its `company_source_ref`). */
|
|
34
34
|
companyRef: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Host-side keys of the person's ACTIVE memberships. Only listed for
|
|
37
|
+
* contacts; reconciliation ends any membership absent from a full sync.
|
|
38
|
+
*/
|
|
39
|
+
companyRefs?: string[];
|
|
35
40
|
id: string;
|
|
36
41
|
sourceRef: string;
|
|
37
42
|
}
|
|
38
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The tenants a person belongs to, as seen in this batch. Present on contact
|
|
45
|
+
* actions only (the `memberships` reconcile option); `companyRef` alongside it
|
|
46
|
+
* is the one picked as primary.
|
|
47
|
+
*/
|
|
48
|
+
interface MembershipSet {
|
|
49
|
+
companyRefs?: string[];
|
|
50
|
+
}
|
|
51
|
+
type SyncAction = ({
|
|
39
52
|
kind: "create";
|
|
40
53
|
sourceRef: string;
|
|
41
54
|
fields: Record<string, string | null>;
|
|
42
55
|
companyRef: string | null;
|
|
43
|
-
} | {
|
|
56
|
+
} & MembershipSet) | ({
|
|
44
57
|
kind: "update";
|
|
45
58
|
id: string;
|
|
46
59
|
sourceRef: string;
|
|
47
60
|
fields: Record<string, string | null>;
|
|
48
61
|
companyRef: string | null;
|
|
49
|
-
} | {
|
|
62
|
+
} & MembershipSet) | ({
|
|
50
63
|
kind: "restore";
|
|
51
64
|
id: string;
|
|
52
65
|
sourceRef: string;
|
|
53
66
|
fields: Record<string, string | null>;
|
|
54
67
|
companyRef: string | null;
|
|
55
|
-
} | {
|
|
68
|
+
} & MembershipSet) | {
|
|
56
69
|
kind: "archive";
|
|
57
70
|
id: string;
|
|
58
71
|
sourceRef: string;
|
|
72
|
+
} | {
|
|
73
|
+
kind: "endMembership";
|
|
74
|
+
id: string;
|
|
75
|
+
sourceRef: string;
|
|
76
|
+
companyRef: string;
|
|
59
77
|
};
|
|
78
|
+
/** A membership with its host tenant key resolved to a `crm_companies.id`. */
|
|
79
|
+
interface ResolvedMembership {
|
|
80
|
+
companyId: string | null;
|
|
81
|
+
companyRef: string;
|
|
82
|
+
}
|
|
83
|
+
interface ResolvedMembershipSet {
|
|
84
|
+
memberships?: ResolvedMembership[];
|
|
85
|
+
}
|
|
60
86
|
/** A persist action with the company ref resolved to a `crm_companies.id`. */
|
|
61
|
-
type PersistAction = {
|
|
87
|
+
type PersistAction = ({
|
|
62
88
|
kind: "create";
|
|
63
89
|
sourceRef: string;
|
|
64
90
|
fields: Record<string, string | null>;
|
|
65
91
|
companyId: string | null;
|
|
66
92
|
companySourceRef: string | null;
|
|
67
|
-
} | {
|
|
93
|
+
} & ResolvedMembershipSet) | ({
|
|
68
94
|
kind: "update";
|
|
69
95
|
id: string;
|
|
70
96
|
sourceRef: string;
|
|
71
97
|
fields: Record<string, string | null>;
|
|
72
98
|
companyId: string | null;
|
|
73
99
|
companySourceRef: string | null;
|
|
74
|
-
} | {
|
|
100
|
+
} & ResolvedMembershipSet) | ({
|
|
75
101
|
kind: "restore";
|
|
76
102
|
id: string;
|
|
77
103
|
sourceRef: string;
|
|
78
104
|
fields: Record<string, string | null>;
|
|
79
105
|
companyId: string | null;
|
|
80
106
|
companySourceRef: string | null;
|
|
81
|
-
} | {
|
|
107
|
+
} & ResolvedMembershipSet) | {
|
|
82
108
|
kind: "archive";
|
|
83
109
|
id: string;
|
|
84
110
|
sourceRef: string;
|
|
111
|
+
} | {
|
|
112
|
+
kind: "endMembership";
|
|
113
|
+
id: string;
|
|
114
|
+
sourceRef: string;
|
|
115
|
+
companyRef: string;
|
|
85
116
|
};
|
|
117
|
+
/** Actions that remove something — only safe when the source set is complete. */
|
|
118
|
+
declare function isRemovalAction(action: {
|
|
119
|
+
kind: string;
|
|
120
|
+
}): boolean;
|
|
86
121
|
/**
|
|
87
122
|
* Diff a view's rows against the current mirror population for one entity.
|
|
88
123
|
*
|
|
89
|
-
* - source
|
|
90
|
-
* - source
|
|
91
|
-
* - source
|
|
92
|
-
* - live mirror row absent from source
|
|
93
|
-
* - archived mirror row absent from source
|
|
124
|
+
* - source identity with no mirror row → create
|
|
125
|
+
* - source identity with a live mirror row → update (overwrite identity fields)
|
|
126
|
+
* - source identity with an archived mirror → restore (re-activate + update)
|
|
127
|
+
* - live mirror row absent from source → archive (soft delete, keep history)
|
|
128
|
+
* - archived mirror row absent from source → no-op
|
|
94
129
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
130
|
+
* Identity is always the source primary key. A `through` (join-table) view
|
|
131
|
+
* fans one person out into a row per tenant; those rows are ONE person, so they
|
|
132
|
+
* collapse into one action. With `memberships` on (contacts), that action also
|
|
133
|
+
* carries every tenant ref the person was seen with, and each tenant the mirror
|
|
134
|
+
* still has as active but the source no longer lists yields `endMembership` —
|
|
135
|
+
* a person leaving one tenant ends that membership and keeps the others.
|
|
101
136
|
*/
|
|
102
137
|
declare function reconcile(source: SourceRow[], mirror: MirrorRecord[], opts?: {
|
|
103
|
-
|
|
138
|
+
memberships?: boolean;
|
|
104
139
|
}): SyncAction[];
|
|
105
140
|
/** DB I/O port. The concrete store lives in the SDK/drizzle package. */
|
|
106
141
|
interface SyncStore {
|
|
@@ -147,6 +182,12 @@ interface SyncResult {
|
|
|
147
182
|
archived: number;
|
|
148
183
|
};
|
|
149
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolve each action's host-side company refs — the primary one and, for
|
|
187
|
+
* contacts, every membership — to `crm_companies.id`s in one lookup. Shared by
|
|
188
|
+
* `runSync` and the cloud push receiver so both persist identical actions.
|
|
189
|
+
*/
|
|
190
|
+
declare function resolveActions(store: SyncStore, actions: SyncAction[]): Promise<PersistAction[]>;
|
|
150
191
|
/**
|
|
151
192
|
* Run a full sync: compile the config's views, then reconcile + persist each,
|
|
152
193
|
* companies first. Returns per-entity and total action counts.
|
|
@@ -181,4 +222,4 @@ declare function previewSync(store: SyncStore, view: CompiledView, opts: {
|
|
|
181
222
|
sourceTable: string;
|
|
182
223
|
}): Promise<PreviewResult>;
|
|
183
224
|
//#endregion
|
|
184
|
-
export { PreviewWarning as a,
|
|
225
|
+
export { PreviewWarning as a, SyncAction as c, computeSampleWarnings as d, isRemovalAction as f, runSync as g, resolveActions as h, PreviewResult as i, SyncResult as l, reconcile as m, MirrorRecord as n, ResolvedMembership as o, previewSync as p, PersistAction as r, SourceRow as s, EntitySyncResult as t, SyncStore as u };
|