clivly 0.13.0 → 0.15.0
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 +13 -6
- package/{adapter-DTMeGGsl.d.cts → adapter-BMAUq4hf.d.mts} +28 -1
- package/{adapter-DhPBRNpi.d.mts → adapter-IH3r_I8v.d.cts} +28 -1
- package/{auth-adapter-C6rDx3XH.d.mts → auth-adapter-Bt1GZiag.d.cts} +1 -1
- package/{auth-adapter-Csk9KkvI.d.cts → auth-adapter-CHy273qb.d.mts} +1 -1
- package/auth-better-auth.d.cts +1 -1
- package/auth-better-auth.d.mts +1 -1
- package/auth-clerk.d.cts +1 -1
- package/auth-clerk.d.mts +1 -1
- package/auth-workos.d.cts +1 -1
- package/auth-workos.d.mts +1 -1
- package/core-adapter.d.cts +1 -1
- package/core-adapter.d.mts +1 -1
- package/core-auth-adapter.d.cts +1 -1
- package/core-auth-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-types.d.cts +1 -1
- package/core-types.d.mts +1 -1
- package/core.cjs +4 -2
- package/core.d.cts +6 -6
- package/core.d.mts +6 -6
- package/core.mjs +3 -3
- package/index.cjs +102 -1
- package/index.mjs +102 -1
- package/{internal-Cmx7Zevf.d.mts → internal-C7NlPM3M.d.mts} +3 -1
- package/{internal-u0Bks5Gr.mjs → internal-CLvZdVj5.mjs} +12 -0
- package/{internal-vH5Rlg6Q.d.cts → internal-D2J4VQWT.d.cts} +3 -1
- package/{internal-BxtvxF7Z.cjs → internal-DsArcqkP.cjs} +12 -0
- package/internal-adapter.d.cts +1 -1
- package/internal-adapter.d.mts +1 -1
- package/internal-auth-adapter.d.cts +1 -1
- package/internal-auth-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-types.d.cts +1 -1
- package/internal-types.d.mts +1 -1
- package/internal.cjs +4 -2
- package/internal.d.cts +6 -6
- package/internal.d.mts +6 -6
- package/internal.mjs +3 -3
- 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
- package/{types-B17TWEOL.d.cts → types-CpAtAtxd.d.cts} +35 -0
- package/{types-B17TWEOL.d.mts → types-CpAtAtxd.d.mts} +35 -0
|
@@ -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 };
|
|
@@ -1,67 +1,103 @@
|
|
|
1
1
|
import { r as compileEntityViews } from "./view-compiler-C-6RydKK.mjs";
|
|
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,
|
|
@@ -211,4 +259,4 @@ async function previewSync(store, view, opts) {
|
|
|
211
259
|
};
|
|
212
260
|
}
|
|
213
261
|
//#endregion
|
|
214
|
-
export {
|
|
262
|
+
export { resolveActions as a, reconcile as i, isRemovalAction as n, runSync as o, previewSync as r, computeSampleWarnings as t };
|
|
@@ -84,12 +84,21 @@ interface Company {
|
|
|
84
84
|
}
|
|
85
85
|
interface Deal {
|
|
86
86
|
closeDate: Date | null;
|
|
87
|
+
/** The organisation this deal belongs to, when one is linked. */
|
|
88
|
+
companyId: string | null;
|
|
87
89
|
contactId: string | null;
|
|
88
90
|
createdAt: Date;
|
|
89
91
|
id: string;
|
|
90
92
|
name: string;
|
|
91
93
|
orgId: string;
|
|
92
94
|
owner: string | null;
|
|
95
|
+
/**
|
|
96
|
+
* The board this deal belongs to. NULL means the org's default pipeline: the
|
|
97
|
+
* sync mirror wrote deals before pipelines existed, and a deal created with
|
|
98
|
+
* no pipeline lands on the default. Stage keys are unique per pipeline, so
|
|
99
|
+
* a board must filter on this, never on stage key alone.
|
|
100
|
+
*/
|
|
101
|
+
pipelineId: string | null;
|
|
93
102
|
position: number | null;
|
|
94
103
|
stage: DealStage;
|
|
95
104
|
/**
|
|
@@ -116,6 +125,11 @@ interface Task {
|
|
|
116
125
|
createdAt: Date;
|
|
117
126
|
createdBy: string | null;
|
|
118
127
|
description: string | null;
|
|
128
|
+
/**
|
|
129
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
130
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
131
|
+
*/
|
|
132
|
+
dueAllDay?: boolean | null;
|
|
119
133
|
dueAt: Date | null;
|
|
120
134
|
entityId: string | null;
|
|
121
135
|
entityType: TaskEntityType | null;
|
|
@@ -203,6 +217,11 @@ interface CreateTaskInput {
|
|
|
203
217
|
assignee?: string;
|
|
204
218
|
createdBy?: string;
|
|
205
219
|
description?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
222
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
223
|
+
*/
|
|
224
|
+
dueAllDay?: boolean | null;
|
|
206
225
|
dueAt?: Date;
|
|
207
226
|
entityId?: string;
|
|
208
227
|
entityType?: TaskEntityType;
|
|
@@ -212,6 +231,11 @@ interface CreateTaskInput {
|
|
|
212
231
|
interface UpdateTaskInput {
|
|
213
232
|
assignee?: string | null;
|
|
214
233
|
description?: string | null;
|
|
234
|
+
/**
|
|
235
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
236
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
237
|
+
*/
|
|
238
|
+
dueAllDay?: boolean | null;
|
|
215
239
|
dueAt?: Date | null;
|
|
216
240
|
entityId?: string | null;
|
|
217
241
|
entityType?: TaskEntityType | null;
|
|
@@ -244,6 +268,17 @@ interface ContactFilters {
|
|
|
244
268
|
* row is no longer sync-provenanced.
|
|
245
269
|
*/
|
|
246
270
|
excludeArchived?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Case-insensitive substring match on name or email.
|
|
273
|
+
*
|
|
274
|
+
* Applied in SQL, not over an already-fetched page: a filter applied after
|
|
275
|
+
* paging returns a partial page and a cursor that skips the rows it dropped,
|
|
276
|
+
* so "page 2" of a search is not the second page of the matches. An empty or
|
|
277
|
+
* whitespace-only value is ignored rather than matching nothing.
|
|
278
|
+
*
|
|
279
|
+
* LIKE metacharacters in the term are escaped and matched literally — a
|
|
280
|
+
* search for `%` finds contacts containing a percent sign, not every row.
|
|
281
|
+
*/
|
|
247
282
|
search?: string;
|
|
248
283
|
source?: RecordSource;
|
|
249
284
|
status?: ContactStatus;
|