@clivly/core 0.1.0 → 0.3.0-next.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 +57 -0
  2. package/dist/adapter.cjs +0 -0
  3. package/dist/adapter.d.cts +46 -0
  4. package/dist/adapter.d.mts +16 -3
  5. package/dist/auth-adapter.cjs +7 -0
  6. package/dist/auth-adapter.d.cts +30 -0
  7. package/dist/drizzle.cjs +32 -0
  8. package/dist/drizzle.d.cts +21 -0
  9. package/dist/drizzle.d.mts +21 -0
  10. package/dist/drizzle.mjs +31 -0
  11. package/dist/entity-config.cjs +350 -0
  12. package/dist/entity-config.d.cts +296 -0
  13. package/dist/entity-config.d.mts +46 -8
  14. package/dist/entity-config.mjs +86 -13
  15. package/dist/entity-heuristics.cjs +442 -0
  16. package/dist/entity-heuristics.d.cts +68 -0
  17. package/dist/entity-heuristics.d.mts +68 -0
  18. package/dist/entity-heuristics.mjs +431 -0
  19. package/dist/index.cjs +27 -0
  20. package/dist/index.d.cts +9 -0
  21. package/dist/index.d.mts +4 -4
  22. package/dist/index.mjs +3 -3
  23. package/dist/mapping-form.cjs +50 -0
  24. package/dist/mapping-form.d.cts +35 -0
  25. package/dist/mapping-score.cjs +97 -0
  26. package/dist/mapping-score.d.cts +49 -0
  27. package/dist/mapping-score.d.mts +49 -0
  28. package/dist/mapping-score.mjs +87 -0
  29. package/dist/mappings-config.cjs +100 -0
  30. package/dist/mappings-config.d.cts +42 -0
  31. package/dist/mappings-config.d.mts +1 -0
  32. package/dist/mappings-config.mjs +21 -1
  33. package/dist/sync-engine.cjs +211 -0
  34. package/dist/sync-engine.d.cts +183 -0
  35. package/dist/sync-engine.d.mts +54 -9
  36. package/dist/sync-engine.mjs +91 -22
  37. package/dist/types.cjs +0 -0
  38. package/dist/types.d.cts +159 -0
  39. package/dist/types.d.mts +12 -1
  40. package/dist/view-compiler.cjs +188 -0
  41. package/dist/view-compiler.d.cts +71 -0
  42. package/dist/view-compiler.d.mts +29 -1
  43. package/dist/view-compiler.mjs +31 -1
  44. package/package.json +116 -23
@@ -0,0 +1,296 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/entity-config.d.ts
4
+ /**
5
+ * Entity Config v2 — the declarative contract that turns Clivly's "schema-aware"
6
+ * promise into a real capability.
7
+ *
8
+ * A CRM entity is no longer a flat table name. It is a *derived entity*: a
9
+ * filtered slice of a host table (`source` + `filter`), a column allowlist
10
+ * (`fields` + `readOnly`), and declared `relationships` to other entities. The
11
+ * same host table can back several entities (e.g. `users` → owners + members)
12
+ * distinguished by `role`.
13
+ *
14
+ * This module is the Phase 1 slice: types, schema, `defineClivlyConfig` (a
15
+ * structural helper) and `validateEntitiesConfig` (semantic validation against
16
+ * the host's discovered schema). It performs NO DB access and generates no SQL —
17
+ * the view compiler (Phase 2) and sync engine (Phase 3) consume this contract.
18
+ */
19
+ /** Canonical CRM concepts a host entity can map onto. */
20
+ declare const CRM_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
21
+ type CrmConcept = (typeof CRM_CONCEPTS)[number];
22
+ /**
23
+ * The canonical `fields` keys the mirror actually persists, per concept — the
24
+ * crm_* columns the sync store writes. An entity's `fields` map keys must be one
25
+ * of these: a non-canonical key (e.g. `fullName` instead of `name`) validates
26
+ * structurally but has no destination column, so the store silently writes an
27
+ * empty value. `validateEntitiesConfig` flags those.
28
+ *
29
+ * This is the single source of truth — the Drizzle store imports it, so the
30
+ * two can't drift (the drift is what made the silent-data-loss trap possible).
31
+ * Concepts absent here aren't materialized yet, so their field keys are
32
+ * unconstrained.
33
+ */
34
+ declare const CANONICAL_FIELDS: {
35
+ readonly contact: readonly ["name", "email", "title", "status"];
36
+ readonly company: readonly ["name", "domain", "industry", "size"];
37
+ readonly deal: readonly ["name", "value", "stage"];
38
+ };
39
+ /**
40
+ * The concepts the mapping→sync pipeline can actually materialize into `crm_*`
41
+ * tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
42
+ * `persist()` throws for any other concept, so offering one (in the mapping
43
+ * editor, in suggestions, or via the upsert API) is a broken affordance.
44
+ *
45
+ * The broader CRM_CONCEPTS set still validates structurally, so previously
46
+ * stored rows for not-yet-materialized concepts keep rendering. When a concept
47
+ * gains a sync-store path, add its canonical fields above and it joins this set
48
+ * automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
49
+ */
50
+ declare const MATERIALIZING_CONCEPTS: readonly ["contact", "company", "deal"];
51
+ type MaterializingConcept = (typeof MATERIALIZING_CONCEPTS)[number];
52
+ /**
53
+ * The org-defined custom-object target. Deliberately NOT a member of
54
+ * CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
55
+ * no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
56
+ * Handled as an explicit branch wherever concepts are switched on, so the
57
+ * canonical-concept invariants (and their drift test) stay intact.
58
+ */
59
+ declare const CUSTOM_CONCEPT: "custom";
60
+ declare const filterValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
61
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
62
+ }, z.core.$strict>, z.ZodObject<{
63
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
64
+ }, z.core.$strict>, z.ZodObject<{
65
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
66
+ }, z.core.$strict>, z.ZodObject<{
67
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
68
+ }, z.core.$strict>, z.ZodObject<{
69
+ isNull: z.ZodLiteral<true>;
70
+ }, z.core.$strict>, z.ZodObject<{
71
+ isNotNull: z.ZodLiteral<true>;
72
+ }, z.core.$strict>]>]>;
73
+ /** `$raw` is a reserved key; a column literally named `$raw` is not addressable. */
74
+ declare const filterSchema: z.ZodUnion<readonly [z.ZodObject<{
75
+ $raw: z.ZodString;
76
+ }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
77
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
78
+ }, z.core.$strict>, z.ZodObject<{
79
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
80
+ }, z.core.$strict>, z.ZodObject<{
81
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
82
+ }, z.core.$strict>, z.ZodObject<{
83
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
84
+ }, z.core.$strict>, z.ZodObject<{
85
+ isNull: z.ZodLiteral<true>;
86
+ }, z.core.$strict>, z.ZodObject<{
87
+ isNotNull: z.ZodLiteral<true>;
88
+ }, z.core.$strict>]>]>>]>;
89
+ type FilterExpr = z.infer<typeof filterSchema>;
90
+ /** A single column predicate value (scalar shorthand or operator object). */
91
+ type FilterValue = z.infer<typeof filterValueSchema>;
92
+ /**
93
+ * How the link between two entities is expressed in the host schema:
94
+ * - `fkOn: "company"` — FK lives on the company table (e.g. organizations.owner_id)
95
+ * - `fkOn: "contact"` — FK lives on the contact table (e.g. users.organization_id)
96
+ * - `through` — a join table sits between them (many-to-many, role often here)
97
+ * - `$raw` — opaque SQL join fragment (escape hatch; not schema-validated)
98
+ *
99
+ * `column` and the join-table keys are `table.column` or bare `column` strings.
100
+ */
101
+ declare const relationshipViaSchema: z.ZodUnion<readonly [z.ZodObject<{
102
+ fkOn: z.ZodEnum<{
103
+ contact: "contact";
104
+ company: "company";
105
+ }>;
106
+ column: z.ZodString;
107
+ }, z.core.$strict>, z.ZodObject<{
108
+ through: z.ZodString;
109
+ localKey: z.ZodString;
110
+ foreignKey: z.ZodString;
111
+ }, z.core.$strict>, z.ZodObject<{
112
+ $raw: z.ZodString;
113
+ }, z.core.$strict>]>;
114
+ type RelationshipVia = z.infer<typeof relationshipViaSchema>;
115
+ declare const relationshipSchema: z.ZodObject<{
116
+ entity: z.ZodString;
117
+ via: z.ZodUnion<readonly [z.ZodObject<{
118
+ fkOn: z.ZodEnum<{
119
+ contact: "contact";
120
+ company: "company";
121
+ }>;
122
+ column: z.ZodString;
123
+ }, z.core.$strict>, z.ZodObject<{
124
+ through: z.ZodString;
125
+ localKey: z.ZodString;
126
+ foreignKey: z.ZodString;
127
+ }, z.core.$strict>, z.ZodObject<{
128
+ $raw: z.ZodString;
129
+ }, z.core.$strict>]>;
130
+ }, z.core.$strict>;
131
+ type RelationshipSpec = z.infer<typeof relationshipSchema>;
132
+ /** A named map of relationships, as stored on an entity / in the mappings row. */
133
+ declare const relationshipsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
134
+ entity: z.ZodString;
135
+ via: z.ZodUnion<readonly [z.ZodObject<{
136
+ fkOn: z.ZodEnum<{
137
+ contact: "contact";
138
+ company: "company";
139
+ }>;
140
+ column: z.ZodString;
141
+ }, z.core.$strict>, z.ZodObject<{
142
+ through: z.ZodString;
143
+ localKey: z.ZodString;
144
+ foreignKey: z.ZodString;
145
+ }, z.core.$strict>, z.ZodObject<{
146
+ $raw: z.ZodString;
147
+ }, z.core.$strict>]>;
148
+ }, z.core.$strict>>;
149
+ type RelationshipsMap = z.infer<typeof relationshipsSchema>;
150
+ declare const entitySchema: z.ZodObject<{
151
+ concept: z.ZodUnion<readonly [z.ZodEnum<{
152
+ contact: "contact";
153
+ company: "company";
154
+ deal: "deal";
155
+ activity: "activity";
156
+ conversation: "conversation";
157
+ note: "note";
158
+ }>, z.ZodLiteral<"custom">]>;
159
+ source: z.ZodString;
160
+ role: z.ZodOptional<z.ZodString>;
161
+ filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
162
+ $raw: z.ZodString;
163
+ }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
164
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
165
+ }, z.core.$strict>, z.ZodObject<{
166
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
167
+ }, z.core.$strict>, z.ZodObject<{
168
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
169
+ }, z.core.$strict>, z.ZodObject<{
170
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
171
+ }, z.core.$strict>, z.ZodObject<{
172
+ isNull: z.ZodLiteral<true>;
173
+ }, z.core.$strict>, z.ZodObject<{
174
+ isNotNull: z.ZodLiteral<true>;
175
+ }, z.core.$strict>]>]>>]>>;
176
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
177
+ readOnly: z.ZodOptional<z.ZodArray<z.ZodString>>;
178
+ relationships: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
179
+ entity: z.ZodString;
180
+ via: z.ZodUnion<readonly [z.ZodObject<{
181
+ fkOn: z.ZodEnum<{
182
+ contact: "contact";
183
+ company: "company";
184
+ }>;
185
+ column: z.ZodString;
186
+ }, z.core.$strict>, z.ZodObject<{
187
+ through: z.ZodString;
188
+ localKey: z.ZodString;
189
+ foreignKey: z.ZodString;
190
+ }, z.core.$strict>, z.ZodObject<{
191
+ $raw: z.ZodString;
192
+ }, z.core.$strict>]>;
193
+ }, z.core.$strict>>>;
194
+ targetObjectTypeId: z.ZodOptional<z.ZodString>;
195
+ }, z.core.$strict>;
196
+ type ClivlyEntityConfig = z.infer<typeof entitySchema>;
197
+ declare const entitiesConfigSchema: z.ZodObject<{
198
+ entities: z.ZodRecord<z.ZodString, z.ZodObject<{
199
+ concept: z.ZodUnion<readonly [z.ZodEnum<{
200
+ contact: "contact";
201
+ company: "company";
202
+ deal: "deal";
203
+ activity: "activity";
204
+ conversation: "conversation";
205
+ note: "note";
206
+ }>, z.ZodLiteral<"custom">]>;
207
+ source: z.ZodString;
208
+ role: z.ZodOptional<z.ZodString>;
209
+ filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
210
+ $raw: z.ZodString;
211
+ }, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
212
+ eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
213
+ }, z.core.$strict>, z.ZodObject<{
214
+ ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
215
+ }, z.core.$strict>, z.ZodObject<{
216
+ in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
217
+ }, z.core.$strict>, z.ZodObject<{
218
+ notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
219
+ }, z.core.$strict>, z.ZodObject<{
220
+ isNull: z.ZodLiteral<true>;
221
+ }, z.core.$strict>, z.ZodObject<{
222
+ isNotNull: z.ZodLiteral<true>;
223
+ }, z.core.$strict>]>]>>]>>;
224
+ fields: z.ZodRecord<z.ZodString, z.ZodString>;
225
+ readOnly: z.ZodOptional<z.ZodArray<z.ZodString>>;
226
+ relationships: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
227
+ entity: z.ZodString;
228
+ via: z.ZodUnion<readonly [z.ZodObject<{
229
+ fkOn: z.ZodEnum<{
230
+ contact: "contact";
231
+ company: "company";
232
+ }>;
233
+ column: z.ZodString;
234
+ }, z.core.$strict>, z.ZodObject<{
235
+ through: z.ZodString;
236
+ localKey: z.ZodString;
237
+ foreignKey: z.ZodString;
238
+ }, z.core.$strict>, z.ZodObject<{
239
+ $raw: z.ZodString;
240
+ }, z.core.$strict>]>;
241
+ }, z.core.$strict>>>;
242
+ targetObjectTypeId: z.ZodOptional<z.ZodString>;
243
+ }, z.core.$strict>>;
244
+ }, z.core.$strict>;
245
+ type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
246
+ /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
247
+ declare class ClivlyConfigError extends Error {
248
+ readonly issues: ConfigValidationError[];
249
+ constructor(issues: ConfigValidationError[]);
250
+ }
251
+ /**
252
+ * Structural helper for `clivly.config.ts`. Gives full type inference on the
253
+ * literal you pass and eagerly parses it, so shape errors surface at config
254
+ * load rather than at sync time.
255
+ *
256
+ * It also catches the silent-data-loss footgun: a non-canonical field key for a
257
+ * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
258
+ * destination column, so the store would write an empty value. By default these
259
+ * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
260
+ * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
261
+ * needs the discovered schema; see `validateEntitiesConfig`.
262
+ */
263
+ declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
264
+ strict?: boolean;
265
+ onWarn?: (message: string) => void;
266
+ }): ClivlyEntitiesConfig;
267
+ /**
268
+ * A host table the SDK reported for discovery. Structurally identical to the
269
+ * SDK's `DiscoveredTable`; duplicated here to keep `@clivly/core` free of an
270
+ * SDK dependency (core is the base contract SDK builds on).
271
+ */
272
+ interface DiscoveredSchemaTable {
273
+ columns: string[];
274
+ name: string;
275
+ }
276
+ interface ConfigValidationError {
277
+ message: string;
278
+ /** Dotted path into the config, e.g. `entities.owner.fields.email`. */
279
+ path: string;
280
+ }
281
+ interface ConfigValidationResult {
282
+ errors: ConfigValidationError[];
283
+ valid: boolean;
284
+ }
285
+ /**
286
+ * Validate a (structurally valid) config against the host's discovered schema:
287
+ * source tables exist, mapped columns exist, `readOnly` ⊆ `fields`, filter
288
+ * columns exist, and relationships point at real entities/tables/columns.
289
+ *
290
+ * `$raw` filters and `$raw` relationship joins are opaque and skipped. Returns
291
+ * every error found (not just the first) so a mapping UI or CLI can show them
292
+ * all at once.
293
+ */
294
+ declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
295
+ //#endregion
296
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -34,7 +34,29 @@ type CrmConcept = (typeof CRM_CONCEPTS)[number];
34
34
  declare const CANONICAL_FIELDS: {
35
35
  readonly contact: readonly ["name", "email", "title", "status"];
36
36
  readonly company: readonly ["name", "domain", "industry", "size"];
37
+ readonly deal: readonly ["name", "value", "stage"];
37
38
  };
39
+ /**
40
+ * The concepts the mapping→sync pipeline can actually materialize into `crm_*`
41
+ * tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
42
+ * `persist()` throws for any other concept, so offering one (in the mapping
43
+ * editor, in suggestions, or via the upsert API) is a broken affordance.
44
+ *
45
+ * The broader CRM_CONCEPTS set still validates structurally, so previously
46
+ * stored rows for not-yet-materialized concepts keep rendering. When a concept
47
+ * gains a sync-store path, add its canonical fields above and it joins this set
48
+ * automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
49
+ */
50
+ declare const MATERIALIZING_CONCEPTS: readonly ["contact", "company", "deal"];
51
+ type MaterializingConcept = (typeof MATERIALIZING_CONCEPTS)[number];
52
+ /**
53
+ * The org-defined custom-object target. Deliberately NOT a member of
54
+ * CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
55
+ * no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
56
+ * Handled as an explicit branch wherever concepts are switched on, so the
57
+ * canonical-concept invariants (and their drift test) stay intact.
58
+ */
59
+ declare const CUSTOM_CONCEPT: "custom";
38
60
  declare const filterValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
39
61
  eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
40
62
  }, z.core.$strict>, z.ZodObject<{
@@ -126,14 +148,14 @@ declare const relationshipsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
126
148
  }, z.core.$strict>>;
127
149
  type RelationshipsMap = z.infer<typeof relationshipsSchema>;
128
150
  declare const entitySchema: z.ZodObject<{
129
- concept: z.ZodEnum<{
151
+ concept: z.ZodUnion<readonly [z.ZodEnum<{
130
152
  contact: "contact";
131
153
  company: "company";
132
154
  deal: "deal";
133
155
  activity: "activity";
134
156
  conversation: "conversation";
135
157
  note: "note";
136
- }>;
158
+ }>, z.ZodLiteral<"custom">]>;
137
159
  source: z.ZodString;
138
160
  role: z.ZodOptional<z.ZodString>;
139
161
  filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
@@ -169,18 +191,19 @@ declare const entitySchema: z.ZodObject<{
169
191
  $raw: z.ZodString;
170
192
  }, z.core.$strict>]>;
171
193
  }, z.core.$strict>>>;
194
+ targetObjectTypeId: z.ZodOptional<z.ZodString>;
172
195
  }, z.core.$strict>;
173
196
  type ClivlyEntityConfig = z.infer<typeof entitySchema>;
174
197
  declare const entitiesConfigSchema: z.ZodObject<{
175
198
  entities: z.ZodRecord<z.ZodString, z.ZodObject<{
176
- concept: z.ZodEnum<{
199
+ concept: z.ZodUnion<readonly [z.ZodEnum<{
177
200
  contact: "contact";
178
201
  company: "company";
179
202
  deal: "deal";
180
203
  activity: "activity";
181
204
  conversation: "conversation";
182
205
  note: "note";
183
- }>;
206
+ }>, z.ZodLiteral<"custom">]>;
184
207
  source: z.ZodString;
185
208
  role: z.ZodOptional<z.ZodString>;
186
209
  filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
@@ -216,16 +239,31 @@ declare const entitiesConfigSchema: z.ZodObject<{
216
239
  $raw: z.ZodString;
217
240
  }, z.core.$strict>]>;
218
241
  }, z.core.$strict>>>;
242
+ targetObjectTypeId: z.ZodOptional<z.ZodString>;
219
243
  }, z.core.$strict>>;
220
244
  }, z.core.$strict>;
221
245
  type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
246
+ /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
247
+ declare class ClivlyConfigError extends Error {
248
+ readonly issues: ConfigValidationError[];
249
+ constructor(issues: ConfigValidationError[]);
250
+ }
222
251
  /**
223
252
  * Structural helper for `clivly.config.ts`. Gives full type inference on the
224
253
  * literal you pass and eagerly parses it, so shape errors surface at config
225
- * load rather than at sync time. Does NOT validate against the host schema —
226
- * that needs the discovered schema; see `validateEntitiesConfig`.
254
+ * load rather than at sync time.
255
+ *
256
+ * It also catches the silent-data-loss footgun: a non-canonical field key for a
257
+ * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
258
+ * destination column, so the store would write an empty value. By default these
259
+ * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
260
+ * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
261
+ * needs the discovered schema; see `validateEntitiesConfig`.
227
262
  */
228
- declare function defineClivlyConfig(config: ClivlyEntitiesConfig): ClivlyEntitiesConfig;
263
+ declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
264
+ strict?: boolean;
265
+ onWarn?: (message: string) => void;
266
+ }): ClivlyEntitiesConfig;
229
267
  /**
230
268
  * A host table the SDK reported for discovery. Structurally identical to the
231
269
  * SDK's `DiscoveredTable`; duplicated here to keep `@clivly/core` free of an
@@ -255,4 +293,4 @@ interface ConfigValidationResult {
255
293
  */
256
294
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
257
295
  //#endregion
258
- export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
296
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -48,9 +48,38 @@ const CANONICAL_FIELDS = {
48
48
  "domain",
49
49
  "industry",
50
50
  "size"
51
+ ],
52
+ deal: [
53
+ "name",
54
+ "value",
55
+ "stage"
51
56
  ]
52
57
  };
53
58
  /**
59
+ * The concepts the mapping→sync pipeline can actually materialize into `crm_*`
60
+ * tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
61
+ * `persist()` throws for any other concept, so offering one (in the mapping
62
+ * editor, in suggestions, or via the upsert API) is a broken affordance.
63
+ *
64
+ * The broader CRM_CONCEPTS set still validates structurally, so previously
65
+ * stored rows for not-yet-materialized concepts keep rendering. When a concept
66
+ * gains a sync-store path, add its canonical fields above and it joins this set
67
+ * automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
68
+ */
69
+ const MATERIALIZING_CONCEPTS = [
70
+ "contact",
71
+ "company",
72
+ "deal"
73
+ ];
74
+ /**
75
+ * The org-defined custom-object target. Deliberately NOT a member of
76
+ * CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
77
+ * no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
78
+ * Handled as an explicit branch wherever concepts are switched on, so the
79
+ * canonical-concept invariants (and their drift test) stay intact.
80
+ */
81
+ const CUSTOM_CONCEPT = "custom";
82
+ /**
54
83
  * Operator forms for a single column predicate. All predicates on an entity are
55
84
  * AND-ed together. `$raw` (see FilterExpr) is the escape hatch for anything the
56
85
  * operators can't express.
@@ -124,8 +153,8 @@ const relationshipSchema = z.strictObject({
124
153
  /** A named map of relationships, as stored on an entity / in the mappings row. */
125
154
  const relationshipsSchema = z.record(z.string(), relationshipSchema);
126
155
  const entitySchema = z.strictObject({
127
- /** Which CRM concept this entity maps onto. */
128
- concept: z.enum(CRM_CONCEPTS),
156
+ /** Which CRM concept this entity maps onto — a canonical concept or "custom". */
157
+ concept: z.union([z.enum(CRM_CONCEPTS), z.literal(CUSTOM_CONCEPT)]),
129
158
  /** Host table (or view) this entity is derived from. */
130
159
  source: z.string().min(1),
131
160
  /**
@@ -143,17 +172,64 @@ const entitySchema = z.strictObject({
143
172
  */
144
173
  readOnly: z.array(z.string()).optional(),
145
174
  /** Named relationships to other entities in this config. */
146
- relationships: z.record(z.string(), relationshipSchema).optional()
175
+ relationships: z.record(z.string(), relationshipSchema).optional(),
176
+ /**
177
+ * For a "custom" entity, the crm_object_types row this materializes into.
178
+ * Required for custom (enforced at the API layer), unused for standard concepts.
179
+ */
180
+ targetObjectTypeId: z.string().min(1).optional()
147
181
  });
148
182
  const entitiesConfigSchema = z.strictObject({ entities: z.record(z.string(), entitySchema).refine((e) => Object.keys(e).length > 0, { message: "at least one entity must be defined" }) });
183
+ /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
184
+ var ClivlyConfigError = class extends Error {
185
+ issues;
186
+ constructor(issues) {
187
+ super(`Clivly config has ${issues.length} non-canonical field mapping(s) that would write empty values at sync time:\n${issues.map((issue) => ` - ${issue.path}: ${issue.message}`).join("\n")}`);
188
+ this.name = "ClivlyConfigError";
189
+ this.issues = issues;
190
+ }
191
+ };
192
+ function nonCanonicalFieldKeys(entity) {
193
+ const canonical = CANONICAL_FIELDS[entity.concept];
194
+ if (!canonical) return [];
195
+ return Object.keys(entity.fields).filter((key) => !canonical.includes(key));
196
+ }
197
+ function nonCanonicalMessage(entity, field) {
198
+ const canonical = CANONICAL_FIELDS[entity.concept] ?? [];
199
+ return `"${field}" is not a canonical ${entity.concept} field (expected one of: ${canonical.join(", ")})`;
200
+ }
201
+ function canonicalFieldIssues(config) {
202
+ const issues = [];
203
+ for (const [key, entity] of Object.entries(config.entities)) for (const field of nonCanonicalFieldKeys(entity)) issues.push({
204
+ path: `entities.${key}.fields.${field}`,
205
+ message: nonCanonicalMessage(entity, field)
206
+ });
207
+ return issues;
208
+ }
209
+ function defaultWarn(message) {
210
+ console.warn(message);
211
+ }
149
212
  /**
150
213
  * Structural helper for `clivly.config.ts`. Gives full type inference on the
151
214
  * literal you pass and eagerly parses it, so shape errors surface at config
152
- * load rather than at sync time. Does NOT validate against the host schema —
153
- * that needs the discovered schema; see `validateEntitiesConfig`.
215
+ * load rather than at sync time.
216
+ *
217
+ * It also catches the silent-data-loss footgun: a non-canonical field key for a
218
+ * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
219
+ * destination column, so the store would write an empty value. By default these
220
+ * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
221
+ * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
222
+ * needs the discovered schema; see `validateEntitiesConfig`.
154
223
  */
155
- function defineClivlyConfig(config) {
156
- return entitiesConfigSchema.parse(config);
224
+ function defineClivlyConfig(config, options) {
225
+ const parsed = entitiesConfigSchema.parse(config);
226
+ const issues = canonicalFieldIssues(parsed);
227
+ if (issues.length > 0) {
228
+ if (options?.strict) throw new ClivlyConfigError(issues);
229
+ const warn = options?.onWarn ?? defaultWarn;
230
+ for (const issue of issues) warn(`⚠ Clivly config: ${issue.message} (${issue.path}) — this field would write an empty value at sync time. Pass { strict: true } to make this throw.`);
231
+ }
232
+ return parsed;
157
233
  }
158
234
  /** Split a `table.column` / bare `column` ref into its parts. */
159
235
  function parseColumnRef(ref) {
@@ -191,11 +267,8 @@ var SchemaValidator = class {
191
267
  }
192
268
  };
193
269
  function validateFields(v, base, entity) {
194
- const canonical = CANONICAL_FIELDS[entity.concept];
195
- for (const [field, column] of Object.entries(entity.fields)) {
196
- v.requireColumn(`${base}.fields.${field}`, entity.source, column);
197
- if (canonical && !canonical.includes(field)) v.report(`${base}.fields.${field}`, `"${field}" is not a canonical ${entity.concept} field (expected one of: ${canonical.join(", ")})`);
198
- }
270
+ for (const [field, column] of Object.entries(entity.fields)) v.requireColumn(`${base}.fields.${field}`, entity.source, column);
271
+ for (const field of nonCanonicalFieldKeys(entity)) v.report(`${base}.fields.${field}`, nonCanonicalMessage(entity, field));
199
272
  }
200
273
  function validateReadOnly(v, base, entity) {
201
274
  for (const field of entity.readOnly ?? []) if (!(field in entity.fields)) v.report(`${base}.readOnly`, `"${field}" is not one of this entity's fields`);
@@ -263,4 +336,4 @@ function validateEntitiesConfig(config, schema) {
263
336
  };
264
337
  }
265
338
  //#endregion
266
- export { CANONICAL_FIELDS, CRM_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
339
+ export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, MATERIALIZING_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };