clivly 0.12.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.
Files changed (44) hide show
  1. package/README.md +10 -3
  2. package/{adapter-DhPBRNpi.d.mts → adapter-B-iBMhl0.d.mts} +13 -0
  3. package/{adapter-DTMeGGsl.d.cts → adapter-JpXSAnj-.d.cts} +13 -0
  4. package/core-adapter.d.cts +1 -1
  5. package/core-adapter.d.mts +1 -1
  6. package/core-sync-engine.cjs +3 -1
  7. package/core-sync-engine.d.cts +2 -2
  8. package/core-sync-engine.d.mts +2 -2
  9. package/core-sync-engine.mjs +2 -2
  10. package/core.cjs +3 -1
  11. package/core.d.cts +3 -3
  12. package/core.d.mts +3 -3
  13. package/core.mjs +2 -2
  14. package/drizzle.cjs +65 -0
  15. package/drizzle.d.cts +51 -1
  16. package/drizzle.d.mts +51 -1
  17. package/drizzle.mjs +64 -1
  18. package/index.cjs +102 -1
  19. package/index.mjs +102 -1
  20. package/internal-adapter.d.cts +1 -1
  21. package/internal-adapter.d.mts +1 -1
  22. package/internal-sync-engine.cjs +3 -1
  23. package/internal-sync-engine.d.cts +2 -2
  24. package/internal-sync-engine.d.mts +2 -2
  25. package/internal-sync-engine.mjs +2 -2
  26. package/internal.cjs +3 -1
  27. package/internal.d.cts +3 -3
  28. package/internal.d.mts +3 -3
  29. package/internal.mjs +2 -2
  30. package/{namespace-probe-CNQKSmxz.d.mts → namespace-probe-DykR0kgh.d.mts} +42 -0
  31. package/{namespace-probe-C5RLl74M.d.cts → namespace-probe-H7yWc0JV.d.cts} +42 -0
  32. package/package.json +1 -1
  33. package/{sdk-C-Pjxj2y.mjs → sdk-DLoVPlGB.mjs} +177 -6
  34. package/{sdk-BvGSxBQc.cjs → sdk-ZMy4C4VK.cjs} +194 -5
  35. package/sdk.cjs +4 -1
  36. package/sdk.d.cts +33 -1
  37. package/sdk.d.mts +33 -1
  38. package/sdk.mjs +2 -2
  39. package/{sync-engine-Dwm_KDUS.cjs → sync-engine-BGVDsrOQ.cjs} +131 -71
  40. package/{sync-engine-V3_UutGv.d.cts → sync-engine-CYsWOujz.d.cts} +63 -22
  41. package/{sync-engine--M9EaONC.d.mts → sync-engine-C_5TpvfX.d.mts} +63 -22
  42. package/{sync-engine-Df2MCs_n.mjs → sync-engine-DH058G3w.mjs} +120 -72
  43. package/vite.d.cts +1 -1
  44. package/vite.d.mts +1 -1
@@ -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 row with no mirror row → create
7
- * - source row with a live mirror row → update (overwrite identity fields)
8
- * - source row with an archived mirror → restore (re-activate + update)
9
- * - live mirror row absent from source → archive (soft delete, keep history)
10
- * - archived mirror row absent from source → no-op
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
- * Source rows are de-duplicated by the reconciliation key (first wins). In
13
- * simple mode (default) the key is `sourceRef`. In composite mode — used for
14
- * `through` (join-table) entities, where the compiled view fans a person out
15
- * into one row per membership — the key is `(sourceRef, companyRef)`, so a
16
- * person in several companies materializes one mirror row per company and a
17
- * single membership leaving archives only that one row.
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 keyOf = (sourceRef, companyRef) => opts.composite ? `${sourceRef}\0${companyRef ?? ""}` : sourceRef;
21
- const seen = /* @__PURE__ */ new Set();
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 row of rows) {
34
- const companyRef = row.companyRef ?? null;
35
- const key = keyOf(row.sourceRef, companyRef);
36
- liveKeys.add(key);
37
- const existing = byKey.get(key);
38
- if (!existing) actions.push({
39
- kind: "create",
40
- sourceRef: row.sourceRef,
41
- fields: row.fields,
42
- companyRef
43
- });
44
- else if (existing.archivedAt) actions.push({
45
- kind: "restore",
46
- id: existing.id,
47
- sourceRef: row.sourceRef,
48
- fields: row.fields,
49
- companyRef
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
- for (const m of mirror) {
60
- const key = keyOf(m.sourceRef, m.companyRef);
61
- if (!(liveKeys.has(key) || m.archivedAt)) actions.push({
62
- kind: "archive",
63
- id: m.id,
64
- sourceRef: m.sourceRef
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) if (action.kind !== "archive" && action.companyRef) refs.add(action.companyRef);
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
- return {
85
- kind: action.kind,
86
- id: "id" in action ? action.id : "",
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, { composite: isCompositeEntity(config, view.entityKey) }).filter((a) => !(options.sample && a.kind === "archive"));
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 { runSync as i, previewSync as n, reconcile as r, computeSampleWarnings as t };
262
+ export { resolveActions as a, reconcile as i, isRemovalAction as n, runSync as o, previewSync as r, computeSampleWarnings as t };
package/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-C5RLl74M.cjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-H7yWc0JV.cjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  export interface ClivlyViteOptions {
package/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-CNQKSmxz.mjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-DykR0kgh.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  export interface ClivlyViteOptions {