@clivly/core 0.8.0 → 0.9.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 (48) hide show
  1. package/README.md +182 -16
  2. package/dist/drizzle.cjs +82 -1
  3. package/dist/drizzle.d.cts +23 -2
  4. package/dist/drizzle.d.mts +23 -2
  5. package/dist/drizzle.mjs +81 -2
  6. package/dist/entity-config-8Fgr6IOH.mjs +666 -0
  7. package/dist/entity-config-CLxI8dVb.d.cts +556 -0
  8. package/dist/entity-config-CLxI8dVb.d.mts +556 -0
  9. package/dist/entity-config-Dh8aEcgj.cjs +779 -0
  10. package/dist/entity-config.cjs +13 -349
  11. package/dist/entity-config.d.cts +2 -295
  12. package/dist/entity-config.d.mts +2 -295
  13. package/dist/entity-config.mjs +2 -339
  14. package/dist/entity-heuristics.cjs +28 -8
  15. package/dist/entity-heuristics.d.cts +2 -12
  16. package/dist/entity-heuristics.d.mts +2 -12
  17. package/dist/entity-heuristics.mjs +28 -8
  18. package/dist/env-file.cjs +59 -0
  19. package/dist/env-file.d.cts +23 -0
  20. package/dist/env-file.d.mts +23 -0
  21. package/dist/env-file.mjs +55 -0
  22. package/dist/index.cjs +83 -2
  23. package/dist/index.d.cts +72 -2
  24. package/dist/index.d.mts +72 -2
  25. package/dist/index.mjs +69 -3
  26. package/dist/integration-state.cjs +242 -0
  27. package/dist/integration-state.d.cts +88 -0
  28. package/dist/integration-state.d.mts +88 -0
  29. package/dist/integration-state.mjs +232 -0
  30. package/dist/mapping-form.d.cts +1 -1
  31. package/dist/mapping-form.d.mts +1 -1
  32. package/dist/mappings-config.cjs +32 -6
  33. package/dist/mappings-config.d.cts +7 -1
  34. package/dist/mappings-config.d.mts +7 -1
  35. package/dist/mappings-config.mjs +32 -6
  36. package/dist/projection-BXBkh7qp.cjs +98 -0
  37. package/dist/projection-C_EMRmZ8.mjs +75 -0
  38. package/dist/sync-engine.cjs +9 -2
  39. package/dist/sync-engine.d.cts +1 -1
  40. package/dist/sync-engine.d.mts +1 -1
  41. package/dist/sync-engine.mjs +9 -2
  42. package/dist/view-compiler-BpNI8Ttw.cjs +299 -0
  43. package/dist/view-compiler-CcqsGxv4.mjs +270 -0
  44. package/dist/view-compiler.cjs +6 -188
  45. package/dist/view-compiler.d.cts +12 -2
  46. package/dist/view-compiler.d.mts +12 -2
  47. package/dist/view-compiler.mjs +2 -185
  48. package/package.json +21 -1
@@ -0,0 +1,232 @@
1
+ //#region src/integration-state.ts
2
+ const INTEGRATION_STEPS = [
3
+ "credential",
4
+ "application",
5
+ "schema",
6
+ "mapping",
7
+ "trigger",
8
+ "sync",
9
+ "widget"
10
+ ];
11
+ const INTEGRATION_STEP_LABELS = {
12
+ credential: "Secure credential",
13
+ application: "Application connected",
14
+ schema: "Schema discovered",
15
+ mapping: "Entities mapped",
16
+ trigger: "Clivly can start a sync",
17
+ sync: "First sync observed",
18
+ widget: "Chat widget installed"
19
+ };
20
+ /** Each step's prerequisites. Ordering is enforced by a test, not convention. */
21
+ const STEP_PREREQUISITES = {
22
+ credential: [],
23
+ application: ["credential"],
24
+ schema: ["application"],
25
+ mapping: ["schema"],
26
+ trigger: ["application"],
27
+ sync: ["mapping", "trigger"],
28
+ widget: ["application"]
29
+ };
30
+ function stepIndex(id) {
31
+ return INTEGRATION_STEPS.indexOf(id);
32
+ }
33
+ /** A heartbeat older than this means the app stopped checking in. */
34
+ const HEARTBEAT_STALE_MS = 600 * 1e3;
35
+ function formatAge(ms) {
36
+ if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s ago`;
37
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
38
+ return `${Math.round(ms / 36e5)}h ago`;
39
+ }
40
+ function heartbeatAge(facts) {
41
+ return facts.lastHeartbeatAt === null ? null : facts.now - facts.lastHeartbeatAt;
42
+ }
43
+ /** Is the raw condition behind each step met, ignoring prerequisites? */
44
+ function isSatisfied(id, facts) {
45
+ const age = heartbeatAge(facts);
46
+ switch (id) {
47
+ case "credential": return facts.hasCredential;
48
+ case "application": return age !== null && age < HEARTBEAT_STALE_MS;
49
+ case "schema": return facts.discoveredTableCount > 0;
50
+ case "mapping": return facts.mappedEntityCount > 0;
51
+ case "trigger": return facts.triggerConfigured;
52
+ case "sync": return facts.lastRun?.ok === true;
53
+ default: return facts.widgetInstalled;
54
+ }
55
+ }
56
+ function evidenceFor(id, facts) {
57
+ const age = heartbeatAge(facts);
58
+ if (id === "application" && age !== null) return `heartbeat ${formatAge(age)}`;
59
+ if (id === "schema" && facts.discoveredTableCount > 0) return `found ${facts.discoveredTableCount} tables`;
60
+ if (id === "mapping" && facts.mappedEntityCount > 0) return `${facts.mappedEntityCount} entity mapped`;
61
+ if (id === "trigger" && facts.triggerConfigured) return "trigger URL and secret set";
62
+ if (id === "sync" && facts.lastRun) return `last run ${formatAge(facts.now - facts.lastRun.at)}`;
63
+ return null;
64
+ }
65
+ function problemFor(id, facts) {
66
+ if (id === "sync" && facts.lastRun && !facts.lastRun.ok) return {
67
+ code: "sync_failed",
68
+ message: facts.lastRun.message ?? "The last sync run failed.",
69
+ fix: "Open the run log to see the host response."
70
+ };
71
+ if (id === "application") {
72
+ const age = heartbeatAge(facts);
73
+ if (age !== null && age >= HEARTBEAT_STALE_MS) return {
74
+ code: "heartbeat_stale",
75
+ message: `Your app stopped checking in ${formatAge(age)}.`,
76
+ fix: "Start your app, then run `npx clivly status`."
77
+ };
78
+ if (age === null && facts.everConnected) return {
79
+ code: "heartbeat_stale",
80
+ message: "Your application stopped checking in.",
81
+ fix: "Start your app, then run `npx clivly status`."
82
+ };
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Walk the ladder once. A step is `blocked` when an earlier step it needs has
88
+ * not been satisfied — a prerequisite statement, not a verdict about this step
89
+ * — and only the first unsatisfied step is `active`, so exactly one root cause
90
+ * ever surfaces.
91
+ */
92
+ function resolveSteps(facts) {
93
+ const satisfied = /* @__PURE__ */ new Set();
94
+ const steps = [];
95
+ let activeTaken = false;
96
+ for (const id of INTEGRATION_STEPS) {
97
+ const unmet = STEP_PREREQUISITES[id].find((need) => !satisfied.has(need));
98
+ const label = INTEGRATION_STEP_LABELS[id];
99
+ if (isSatisfied(id, facts)) {
100
+ satisfied.add(id);
101
+ steps.push({
102
+ id,
103
+ label,
104
+ state: "done",
105
+ blockedBy: null,
106
+ evidence: evidenceFor(id, facts),
107
+ problem: null
108
+ });
109
+ continue;
110
+ }
111
+ if (unmet !== void 0) {
112
+ steps.push({
113
+ id,
114
+ label,
115
+ state: "blocked",
116
+ blockedBy: unmet,
117
+ evidence: null,
118
+ problem: null
119
+ });
120
+ continue;
121
+ }
122
+ const problem = problemFor(id, facts);
123
+ let state = "active";
124
+ if (problem) state = "failed";
125
+ else if (id === "widget") state = "skipped";
126
+ const claimsAttention = state === "active" || state === "failed";
127
+ steps.push({
128
+ id,
129
+ label,
130
+ state: activeTaken && state === "active" ? "blocked" : state,
131
+ blockedBy: null,
132
+ evidence: evidenceFor(id, facts),
133
+ problem: activeTaken ? null : problem
134
+ });
135
+ if (claimsAttention) activeTaken = true;
136
+ }
137
+ return steps;
138
+ }
139
+ const STEP_STATUS_WHEN_ACTIVE = {
140
+ credential: "not_configured",
141
+ application: "awaiting_connection",
142
+ schema: "connected",
143
+ mapping: "mapping_required",
144
+ trigger: "trigger_required",
145
+ sync: "ready_to_sync",
146
+ widget: "healthy"
147
+ };
148
+ /**
149
+ * Collapse the ladder to the single status the surfaces switch on. Live
150
+ * conditions — a run in flight, a failure, a heartbeat gone quiet — win over
151
+ * ladder position, because they describe the integration as it is now rather
152
+ * than how far setup once got.
153
+ */
154
+ function deriveStatus(steps, facts) {
155
+ if (facts.syncInFlight) return "syncing";
156
+ if (steps.find((step) => step.id === "application")?.problem?.code === "heartbeat_stale") return "disconnected";
157
+ if (steps.some((step) => step.state === "failed")) return "error";
158
+ const blocking = steps.find((step) => step.state === "active" || step.state === "blocked");
159
+ if (blocking) return STEP_STATUS_WHEN_ACTIVE[blocking.id];
160
+ return facts.warnings.length > 0 ? "warning" : "healthy";
161
+ }
162
+ /**
163
+ * The command that installs Clivly into a host app. Exported because the connect
164
+ * dialog prints it too, and it must be the same string as the one the ladder
165
+ * recommends — that pairing is what C-05 broke.
166
+ */
167
+ const INSTALL_COMMAND = "npx clivly init";
168
+ const NEXT_BY_STEP = {
169
+ credential: {
170
+ label: "Create a credential",
171
+ command: null,
172
+ envFile: null
173
+ },
174
+ application: {
175
+ label: "Install Clivly in your app",
176
+ command: INSTALL_COMMAND,
177
+ envFile: ".env.local"
178
+ },
179
+ schema: {
180
+ label: "Check what the SDK reported",
181
+ command: "npx clivly status",
182
+ envFile: null
183
+ },
184
+ mapping: {
185
+ label: "Choose the table holding your people",
186
+ command: null,
187
+ envFile: null
188
+ },
189
+ trigger: {
190
+ label: "Let Clivly start a sync",
191
+ command: null,
192
+ envFile: null
193
+ },
194
+ sync: {
195
+ label: "Run the first sync",
196
+ command: "npx clivly sync",
197
+ envFile: null
198
+ },
199
+ widget: {
200
+ label: "Install the chat widget",
201
+ command: null,
202
+ envFile: null
203
+ }
204
+ };
205
+ /**
206
+ * THE one producer of `next.command`. Six surfaces previously each invented
207
+ * their own "run this next" string, and one of them named a read-only command
208
+ * as the fix for a write problem (C-05). If you need a command for the
209
+ * next-action slot, call this — do not write another one.
210
+ */
211
+ function nextAction(steps, status) {
212
+ if (status === "healthy" || status === "syncing") return null;
213
+ const blocking = steps.find((step) => step.state === "failed") ?? steps.find((step) => step.state === "active");
214
+ if (!blocking) return null;
215
+ if (blocking.problem?.code === "heartbeat_stale") return {
216
+ label: "Reconnect your app",
217
+ command: "npx clivly status",
218
+ envFile: null
219
+ };
220
+ return NEXT_BY_STEP[blocking.id];
221
+ }
222
+ function computeIntegrationState(facts) {
223
+ const steps = resolveSteps(facts);
224
+ const status = deriveStatus(steps, facts);
225
+ return {
226
+ status,
227
+ steps,
228
+ next: nextAction(steps, status)
229
+ };
230
+ }
231
+ //#endregion
232
+ export { INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, STEP_PREREQUISITES, computeIntegrationState, deriveStatus, formatAge, nextAction, resolveSteps, stepIndex };
@@ -1,4 +1,4 @@
1
- import { FilterExpr, RelationshipVia, RelationshipsMap } from "./entity-config.cjs";
1
+ import { b as RelationshipsMap, m as FilterExpr, y as RelationshipVia } from "./entity-config-CLxI8dVb.cjs";
2
2
  //#region src/mapping-form.d.ts
3
3
  /**
4
4
  * A relationship the mapping UI can offer or has configured, in the shape the
@@ -1,4 +1,4 @@
1
- import { FilterExpr, RelationshipVia, RelationshipsMap } from "./entity-config.mjs";
1
+ import { b as RelationshipsMap, m as FilterExpr, y as RelationshipVia } from "./entity-config-CLxI8dVb.mjs";
2
2
  //#region src/mapping-form.d.ts
3
3
  /**
4
4
  * A relationship the mapping UI can offer or has configured, in the shape the
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config.cjs");
2
+ const require_entity_config = require("./entity-config-Dh8aEcgj.cjs");
3
3
  //#region src/mappings-config.ts
4
4
  const RESERVED_FIELD = "id";
5
5
  /**
@@ -22,7 +22,9 @@ function canonicalFields(concept, fieldMap) {
22
22
  const DEFAULT_ROLE = "primary";
23
23
  const ACTIVE_STATUS = "active";
24
24
  function isEligible(row) {
25
- return (row.status ?? ACTIVE_STATUS) === ACTIVE_STATUS && Object.keys(row.fieldMap ?? {}).length > 0;
25
+ if ((row.status ?? ACTIVE_STATUS) !== ACTIVE_STATUS) return false;
26
+ if (row.projection) return Object.keys(row.projection.fieldBindings ?? {}).length > 0;
27
+ return Object.keys(row.fieldMap ?? {}).length > 0;
26
28
  }
27
29
  /**
28
30
  * Preferred key for a row: the bare source table when it backs a single entity,
@@ -46,13 +48,28 @@ function uniquify(base, used) {
46
48
  used.add(key);
47
49
  return key;
48
50
  }
51
+ /**
52
+ * A projection's `fields` map, derived mechanically from its bindings.
53
+ *
54
+ * `ClivlyEntityConfig.fields` is required and read by existing consumers, so a
55
+ * projection entity must populate it. The values are ref *tokens*
56
+ * (`"base.full_name"`), never host column names — the compiler reads
57
+ * `projection.fieldBindings`, and `validateEntitiesConfig` skips the legacy
58
+ * column check for projection entities precisely because of this.
59
+ */
60
+ function projectionFields(projection) {
61
+ const fields = {};
62
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) fields[field] = `${binding.from.table}.${binding.from.column}`;
63
+ return fields;
64
+ }
49
65
  function buildEntity(row, refIndex) {
50
66
  const entity = {
51
67
  concept: row.targetConcept,
52
- source: row.sourceTable,
68
+ source: row.projection?.baseTable ?? row.sourceTable,
53
69
  role: row.role ?? DEFAULT_ROLE,
54
- fields: canonicalFields(row.targetConcept, row.fieldMap)
70
+ fields: row.projection ? projectionFields(row.projection) : canonicalFields(row.targetConcept, row.fieldMap)
55
71
  };
72
+ if (row.projection) entity.projection = row.projection;
56
73
  if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
57
74
  if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
58
75
  if (row.relationships && Object.keys(row.relationships).length > 0) {
@@ -77,11 +94,16 @@ function buildEntity(row, refIndex) {
77
94
  function mappingsToEntitiesConfig(rows) {
78
95
  const eligible = rows.filter(isEligible);
79
96
  const tableCounts = /* @__PURE__ */ new Map();
80
- for (const row of eligible) tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
97
+ for (const row of eligible) {
98
+ if (row.projection) continue;
99
+ tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
100
+ }
81
101
  const used = /* @__PURE__ */ new Set();
82
102
  const tableAnchor = /* @__PURE__ */ new Map();
83
103
  const assigned = [];
84
- for (const row of eligible) {
104
+ const legacyRows = eligible.filter((row) => !row.projection);
105
+ const projectionRows = eligible.filter((row) => row.projection);
106
+ for (const row of legacyRows) {
85
107
  const key = uniquify(preferredKey(row, tableCounts), used);
86
108
  assigned.push({
87
109
  row,
@@ -89,6 +111,10 @@ function mappingsToEntitiesConfig(rows) {
89
111
  });
90
112
  if ((row.role ?? DEFAULT_ROLE) === DEFAULT_ROLE || !tableAnchor.has(row.sourceTable)) tableAnchor.set(row.sourceTable, key);
91
113
  }
114
+ for (const row of projectionRows) assigned.push({
115
+ row,
116
+ key: uniquify(row.projection?.entityKey ?? row.sourceTable, used)
117
+ });
92
118
  const refIndex = /* @__PURE__ */ new Map();
93
119
  for (const { key } of assigned) refIndex.set(key, key);
94
120
  for (const [table, key] of tableAnchor) if (!refIndex.has(table)) refIndex.set(table, key);
@@ -1,4 +1,4 @@
1
- import { ClivlyEntitiesConfig } from "./entity-config.cjs";
1
+ import { M as ProjectionMapping, a as ClivlyEntitiesConfig } from "./entity-config-CLxI8dVb.cjs";
2
2
  //#region src/mappings-config.d.ts
3
3
  /**
4
4
  * Mappings → Entity Config bridge.
@@ -23,6 +23,12 @@ import { ClivlyEntitiesConfig } from "./entity-config.cjs";
23
23
  interface EntityMappingRow {
24
24
  fieldMap: Record<string, string>;
25
25
  filter?: Record<string, unknown> | null;
26
+ /**
27
+ * Advanced mapping: a base table plus explicit joins. When set, this — not
28
+ * `fieldMap` — is the authored source of truth, and `sourceTable` merely
29
+ * mirrors `projection.baseTable`.
30
+ */
31
+ projection?: ProjectionMapping | null;
26
32
  relationships?: Record<string, unknown> | null;
27
33
  role?: string | null;
28
34
  sourceTable: string;
@@ -1,4 +1,4 @@
1
- import { ClivlyEntitiesConfig } from "./entity-config.mjs";
1
+ import { M as ProjectionMapping, a as ClivlyEntitiesConfig } from "./entity-config-CLxI8dVb.mjs";
2
2
  //#region src/mappings-config.d.ts
3
3
  /**
4
4
  * Mappings → Entity Config bridge.
@@ -23,6 +23,12 @@ import { ClivlyEntitiesConfig } from "./entity-config.mjs";
23
23
  interface EntityMappingRow {
24
24
  fieldMap: Record<string, string>;
25
25
  filter?: Record<string, unknown> | null;
26
+ /**
27
+ * Advanced mapping: a base table plus explicit joins. When set, this — not
28
+ * `fieldMap` — is the authored source of truth, and `sourceTable` merely
29
+ * mirrors `projection.baseTable`.
30
+ */
31
+ projection?: ProjectionMapping | null;
26
32
  relationships?: Record<string, unknown> | null;
27
33
  role?: string | null;
28
34
  sourceTable: string;
@@ -1,4 +1,4 @@
1
- import { CANONICAL_FIELDS } from "./entity-config.mjs";
1
+ import { t as CANONICAL_FIELDS } from "./entity-config-8Fgr6IOH.mjs";
2
2
  //#region src/mappings-config.ts
3
3
  const RESERVED_FIELD = "id";
4
4
  /**
@@ -21,7 +21,9 @@ function canonicalFields(concept, fieldMap) {
21
21
  const DEFAULT_ROLE = "primary";
22
22
  const ACTIVE_STATUS = "active";
23
23
  function isEligible(row) {
24
- return (row.status ?? ACTIVE_STATUS) === ACTIVE_STATUS && Object.keys(row.fieldMap ?? {}).length > 0;
24
+ if ((row.status ?? ACTIVE_STATUS) !== ACTIVE_STATUS) return false;
25
+ if (row.projection) return Object.keys(row.projection.fieldBindings ?? {}).length > 0;
26
+ return Object.keys(row.fieldMap ?? {}).length > 0;
25
27
  }
26
28
  /**
27
29
  * Preferred key for a row: the bare source table when it backs a single entity,
@@ -45,13 +47,28 @@ function uniquify(base, used) {
45
47
  used.add(key);
46
48
  return key;
47
49
  }
50
+ /**
51
+ * A projection's `fields` map, derived mechanically from its bindings.
52
+ *
53
+ * `ClivlyEntityConfig.fields` is required and read by existing consumers, so a
54
+ * projection entity must populate it. The values are ref *tokens*
55
+ * (`"base.full_name"`), never host column names — the compiler reads
56
+ * `projection.fieldBindings`, and `validateEntitiesConfig` skips the legacy
57
+ * column check for projection entities precisely because of this.
58
+ */
59
+ function projectionFields(projection) {
60
+ const fields = {};
61
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) fields[field] = `${binding.from.table}.${binding.from.column}`;
62
+ return fields;
63
+ }
48
64
  function buildEntity(row, refIndex) {
49
65
  const entity = {
50
66
  concept: row.targetConcept,
51
- source: row.sourceTable,
67
+ source: row.projection?.baseTable ?? row.sourceTable,
52
68
  role: row.role ?? DEFAULT_ROLE,
53
- fields: canonicalFields(row.targetConcept, row.fieldMap)
69
+ fields: row.projection ? projectionFields(row.projection) : canonicalFields(row.targetConcept, row.fieldMap)
54
70
  };
71
+ if (row.projection) entity.projection = row.projection;
55
72
  if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
56
73
  if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
57
74
  if (row.relationships && Object.keys(row.relationships).length > 0) {
@@ -76,11 +93,16 @@ function buildEntity(row, refIndex) {
76
93
  function mappingsToEntitiesConfig(rows) {
77
94
  const eligible = rows.filter(isEligible);
78
95
  const tableCounts = /* @__PURE__ */ new Map();
79
- for (const row of eligible) tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
96
+ for (const row of eligible) {
97
+ if (row.projection) continue;
98
+ tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
99
+ }
80
100
  const used = /* @__PURE__ */ new Set();
81
101
  const tableAnchor = /* @__PURE__ */ new Map();
82
102
  const assigned = [];
83
- for (const row of eligible) {
103
+ const legacyRows = eligible.filter((row) => !row.projection);
104
+ const projectionRows = eligible.filter((row) => row.projection);
105
+ for (const row of legacyRows) {
84
106
  const key = uniquify(preferredKey(row, tableCounts), used);
85
107
  assigned.push({
86
108
  row,
@@ -88,6 +110,10 @@ function mappingsToEntitiesConfig(rows) {
88
110
  });
89
111
  if ((row.role ?? DEFAULT_ROLE) === DEFAULT_ROLE || !tableAnchor.has(row.sourceTable)) tableAnchor.set(row.sourceTable, key);
90
112
  }
113
+ for (const row of projectionRows) assigned.push({
114
+ row,
115
+ key: uniquify(row.projection?.entityKey ?? row.sourceTable, used)
116
+ });
91
117
  const refIndex = /* @__PURE__ */ new Map();
92
118
  for (const { key } of assigned) refIndex.set(key, key);
93
119
  for (const [table, key] of tableAnchor) if (!refIndex.has(table)) refIndex.set(table, key);
@@ -0,0 +1,98 @@
1
+ let zod = require("zod");
2
+ //#region src/projection.ts
3
+ /**
4
+ * Projection-backed custom mappings — one custom object derived from a base
5
+ * table plus explicit joins.
6
+ *
7
+ * A projection is deliberately narrow: equality joins only, direct column
8
+ * bindings only, no expressions and no aggregation. Everything a user can
9
+ * express here compiles to a single read-only view whose `id` column is
10
+ * provably unique per row, which is what lets the sync engine consume it
11
+ * unchanged.
12
+ *
13
+ * Refs address tables symbolically: `"base"` for the base table, or a join's
14
+ * `key`. Raw table names are never accepted in a ref, so a projection can
15
+ * never reach a table it did not declare.
16
+ */
17
+ /** The reserved ref name for the projection's base table. */
18
+ const BASE_REF = "base";
19
+ /** Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs. */
20
+ const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
21
+ const projectionRefSchema = zod.z.strictObject({
22
+ table: zod.z.string().min(1),
23
+ column: zod.z.string().min(1)
24
+ });
25
+ const projectionJoinConditionSchema = zod.z.strictObject({
26
+ left: projectionRefSchema,
27
+ op: zod.z.literal("eq"),
28
+ right: projectionRefSchema
29
+ });
30
+ const projectionJoinSchema = zod.z.strictObject({
31
+ /** SQL alias and ref target. Unique within the projection. */
32
+ key: zod.z.string().regex(PROJECTION_KEY_PATTERN),
33
+ table: zod.z.string().min(1),
34
+ type: zod.z.enum(["inner", "left"]),
35
+ on: zod.z.array(projectionJoinConditionSchema).min(1)
36
+ });
37
+ const projectionFieldBindingSchema = zod.z.strictObject({ from: projectionRefSchema });
38
+ const projectionIdentitySchema = zod.z.union([
39
+ zod.z.strictObject({
40
+ kind: zod.z.literal("base_pk"),
41
+ /** Defaults to the discovered single-column PK of the base table. */
42
+ column: zod.z.string().min(1).optional()
43
+ }),
44
+ zod.z.strictObject({
45
+ kind: zod.z.literal("column"),
46
+ from: projectionRefSchema
47
+ }),
48
+ zod.z.strictObject({
49
+ kind: zod.z.literal("composite"),
50
+ parts: zod.z.array(projectionRefSchema).min(2)
51
+ })
52
+ ]);
53
+ const projectionSchema = zod.z.strictObject({
54
+ version: zod.z.literal(1),
55
+ kind: zod.z.literal("projection"),
56
+ entityKey: zod.z.string().regex(PROJECTION_KEY_PATTERN),
57
+ baseTable: zod.z.string().min(1),
58
+ joins: zod.z.array(projectionJoinSchema),
59
+ fieldBindings: zod.z.record(zod.z.string(), projectionFieldBindingSchema).refine((b) => Object.keys(b).length > 0, { message: "at least one field must be bound" }),
60
+ identity: projectionIdentitySchema
61
+ });
62
+ /**
63
+ * Every column ref an identity depends on. `basePkColumn` is the base table's
64
+ * discovered PK, used only when a `base_pk` identity omits an explicit column.
65
+ */
66
+ function identityRefs(identity, basePkColumn) {
67
+ if (identity.kind === "base_pk") return [{
68
+ table: BASE_REF,
69
+ column: identity.column ?? basePkColumn
70
+ }];
71
+ if (identity.kind === "column") return [identity.from];
72
+ return identity.parts;
73
+ }
74
+ //#endregion
75
+ Object.defineProperty(exports, "BASE_REF", {
76
+ enumerable: true,
77
+ get: function() {
78
+ return BASE_REF;
79
+ }
80
+ });
81
+ Object.defineProperty(exports, "PROJECTION_KEY_PATTERN", {
82
+ enumerable: true,
83
+ get: function() {
84
+ return PROJECTION_KEY_PATTERN;
85
+ }
86
+ });
87
+ Object.defineProperty(exports, "identityRefs", {
88
+ enumerable: true,
89
+ get: function() {
90
+ return identityRefs;
91
+ }
92
+ });
93
+ Object.defineProperty(exports, "projectionSchema", {
94
+ enumerable: true,
95
+ get: function() {
96
+ return projectionSchema;
97
+ }
98
+ });
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ //#region src/projection.ts
3
+ /**
4
+ * Projection-backed custom mappings — one custom object derived from a base
5
+ * table plus explicit joins.
6
+ *
7
+ * A projection is deliberately narrow: equality joins only, direct column
8
+ * bindings only, no expressions and no aggregation. Everything a user can
9
+ * express here compiles to a single read-only view whose `id` column is
10
+ * provably unique per row, which is what lets the sync engine consume it
11
+ * unchanged.
12
+ *
13
+ * Refs address tables symbolically: `"base"` for the base table, or a join's
14
+ * `key`. Raw table names are never accepted in a ref, so a projection can
15
+ * never reach a table it did not declare.
16
+ */
17
+ /** The reserved ref name for the projection's base table. */
18
+ const BASE_REF = "base";
19
+ /** Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs. */
20
+ const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
21
+ const projectionRefSchema = z.strictObject({
22
+ table: z.string().min(1),
23
+ column: z.string().min(1)
24
+ });
25
+ const projectionJoinConditionSchema = z.strictObject({
26
+ left: projectionRefSchema,
27
+ op: z.literal("eq"),
28
+ right: projectionRefSchema
29
+ });
30
+ const projectionJoinSchema = z.strictObject({
31
+ /** SQL alias and ref target. Unique within the projection. */
32
+ key: z.string().regex(PROJECTION_KEY_PATTERN),
33
+ table: z.string().min(1),
34
+ type: z.enum(["inner", "left"]),
35
+ on: z.array(projectionJoinConditionSchema).min(1)
36
+ });
37
+ const projectionFieldBindingSchema = z.strictObject({ from: projectionRefSchema });
38
+ const projectionIdentitySchema = z.union([
39
+ z.strictObject({
40
+ kind: z.literal("base_pk"),
41
+ /** Defaults to the discovered single-column PK of the base table. */
42
+ column: z.string().min(1).optional()
43
+ }),
44
+ z.strictObject({
45
+ kind: z.literal("column"),
46
+ from: projectionRefSchema
47
+ }),
48
+ z.strictObject({
49
+ kind: z.literal("composite"),
50
+ parts: z.array(projectionRefSchema).min(2)
51
+ })
52
+ ]);
53
+ const projectionSchema = z.strictObject({
54
+ version: z.literal(1),
55
+ kind: z.literal("projection"),
56
+ entityKey: z.string().regex(PROJECTION_KEY_PATTERN),
57
+ baseTable: z.string().min(1),
58
+ joins: z.array(projectionJoinSchema),
59
+ fieldBindings: z.record(z.string(), projectionFieldBindingSchema).refine((b) => Object.keys(b).length > 0, { message: "at least one field must be bound" }),
60
+ identity: projectionIdentitySchema
61
+ });
62
+ /**
63
+ * Every column ref an identity depends on. `basePkColumn` is the base table's
64
+ * discovered PK, used only when a `base_pk` identity omits an explicit column.
65
+ */
66
+ function identityRefs(identity, basePkColumn) {
67
+ if (identity.kind === "base_pk") return [{
68
+ table: BASE_REF,
69
+ column: identity.column ?? basePkColumn
70
+ }];
71
+ if (identity.kind === "column") return [identity.from];
72
+ return identity.parts;
73
+ }
74
+ //#endregion
75
+ export { projectionSchema as i, PROJECTION_KEY_PATTERN as n, identityRefs as r, BASE_REF as t };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_view_compiler = require("./view-compiler.cjs");
2
+ const require_view_compiler = require("./view-compiler-BpNI8Ttw.cjs");
3
3
  //#region src/sync-engine.ts
4
4
  /**
5
5
  * Diff a view's rows against the current mirror population for one entity.
@@ -171,10 +171,17 @@ function isEmptyValue(value) {
171
171
  */
172
172
  function computeSampleWarnings(rows, requiredFields, sourceTable) {
173
173
  if (rows.length === 0) return [{ message: `no rows found in ${sourceTable}` }];
174
+ const seenRefs = /* @__PURE__ */ new Set();
175
+ const duplicateRefs = /* @__PURE__ */ new Set();
176
+ for (const row of rows) {
177
+ if (seenRefs.has(row.sourceRef)) duplicateRefs.add(row.sourceRef);
178
+ seenRefs.add(row.sourceRef);
179
+ }
180
+ const identityWarnings = duplicateRefs.size > 0 ? [{ message: `duplicate id in ${duplicateRefs.size} of ${rows.length} sampled rows — identity is not unique, so sync would collapse these into one record` }] : [];
174
181
  const required = new Set(requiredFields);
175
182
  const fields = new Set(requiredFields);
176
183
  for (const row of rows) for (const key of Object.keys(row.fields)) fields.add(key);
177
- const warnings = [];
184
+ const warnings = [...identityWarnings];
178
185
  for (const field of [...fields].sort()) {
179
186
  const emptyCount = rows.filter((row) => isEmptyValue(row.fields[field])).length;
180
187
  if (required.has(field)) {
@@ -1,4 +1,4 @@
1
- import { ClivlyEntitiesConfig } from "./entity-config.cjs";
1
+ import { a as ClivlyEntitiesConfig } from "./entity-config-CLxI8dVb.cjs";
2
2
  import { CompileOptions, CompiledView } from "./view-compiler.cjs";
3
3
  //#region src/sync-engine.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { ClivlyEntitiesConfig } from "./entity-config.mjs";
1
+ import { a as ClivlyEntitiesConfig } from "./entity-config-CLxI8dVb.mjs";
2
2
  import { CompileOptions, CompiledView } from "./view-compiler.mjs";
3
3
  //#region src/sync-engine.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { compileEntityViews } from "./view-compiler.mjs";
1
+ import { r as compileEntityViews } from "./view-compiler-CcqsGxv4.mjs";
2
2
  //#region src/sync-engine.ts
3
3
  /**
4
4
  * Diff a view's rows against the current mirror population for one entity.
@@ -170,10 +170,17 @@ function isEmptyValue(value) {
170
170
  */
171
171
  function computeSampleWarnings(rows, requiredFields, sourceTable) {
172
172
  if (rows.length === 0) return [{ message: `no rows found in ${sourceTable}` }];
173
+ const seenRefs = /* @__PURE__ */ new Set();
174
+ const duplicateRefs = /* @__PURE__ */ new Set();
175
+ for (const row of rows) {
176
+ if (seenRefs.has(row.sourceRef)) duplicateRefs.add(row.sourceRef);
177
+ seenRefs.add(row.sourceRef);
178
+ }
179
+ const identityWarnings = duplicateRefs.size > 0 ? [{ message: `duplicate id in ${duplicateRefs.size} of ${rows.length} sampled rows — identity is not unique, so sync would collapse these into one record` }] : [];
173
180
  const required = new Set(requiredFields);
174
181
  const fields = new Set(requiredFields);
175
182
  for (const row of rows) for (const key of Object.keys(row.fields)) fields.add(key);
176
- const warnings = [];
183
+ const warnings = [...identityWarnings];
177
184
  for (const field of [...fields].sort()) {
178
185
  const emptyCount = rows.filter((row) => isEmptyValue(row.fields[field])).length;
179
186
  if (required.has(field)) {