@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.
- package/README.md +57 -0
- package/dist/adapter.cjs +0 -0
- package/dist/adapter.d.cts +46 -0
- package/dist/adapter.d.mts +16 -3
- package/dist/auth-adapter.cjs +7 -0
- package/dist/auth-adapter.d.cts +30 -0
- package/dist/drizzle.cjs +32 -0
- package/dist/drizzle.d.cts +21 -0
- package/dist/drizzle.d.mts +21 -0
- package/dist/drizzle.mjs +31 -0
- package/dist/entity-config.cjs +350 -0
- package/dist/entity-config.d.cts +296 -0
- package/dist/entity-config.d.mts +46 -8
- package/dist/entity-config.mjs +86 -13
- package/dist/entity-heuristics.cjs +442 -0
- package/dist/entity-heuristics.d.cts +68 -0
- package/dist/entity-heuristics.d.mts +68 -0
- package/dist/entity-heuristics.mjs +431 -0
- package/dist/index.cjs +27 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/mapping-form.cjs +50 -0
- package/dist/mapping-form.d.cts +35 -0
- package/dist/mapping-score.cjs +97 -0
- package/dist/mapping-score.d.cts +49 -0
- package/dist/mapping-score.d.mts +49 -0
- package/dist/mapping-score.mjs +87 -0
- package/dist/mappings-config.cjs +100 -0
- package/dist/mappings-config.d.cts +42 -0
- package/dist/mappings-config.d.mts +1 -0
- package/dist/mappings-config.mjs +21 -1
- package/dist/sync-engine.cjs +211 -0
- package/dist/sync-engine.d.cts +183 -0
- package/dist/sync-engine.d.mts +54 -9
- package/dist/sync-engine.mjs +91 -22
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +159 -0
- package/dist/types.d.mts +12 -1
- package/dist/view-compiler.cjs +188 -0
- package/dist/view-compiler.d.cts +71 -0
- package/dist/view-compiler.d.mts +29 -1
- package/dist/view-compiler.mjs +31 -1
- 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 };
|
package/dist/entity-config.d.mts
CHANGED
|
@@ -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.
|
|
226
|
-
*
|
|
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
|
|
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 };
|
package/dist/entity-config.mjs
CHANGED
|
@@ -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.
|
|
153
|
-
*
|
|
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
|
-
|
|
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
|
|
195
|
-
for (const
|
|
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 };
|