@clivly/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/adapter.cjs +0 -0
- package/dist/adapter.d.cts +36 -0
- package/dist/adapter.d.mts +4 -1
- 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 +314 -0
- package/dist/entity-config.d.cts +272 -0
- package/dist/entity-config.d.mts +18 -4
- package/dist/entity-config.mjs +49 -10
- package/dist/entity-heuristics.cjs +344 -0
- package/dist/entity-heuristics.d.cts +43 -0
- package/dist/entity-heuristics.d.mts +43 -0
- package/dist/entity-heuristics.mjs +335 -0
- package/dist/index.cjs +26 -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/mappings-config.cjs +99 -0
- package/dist/mappings-config.d.cts +41 -0
- package/dist/mappings-config.mjs +20 -1
- package/dist/sync-engine.cjs +161 -0
- package/dist/sync-engine.d.cts +148 -0
- package/dist/sync-engine.d.mts +14 -4
- package/dist/sync-engine.mjs +39 -18
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +156 -0
- package/dist/types.d.mts +9 -1
- package/dist/view-compiler.cjs +188 -0
- package/dist/view-compiler.d.cts +64 -0
- package/dist/view-compiler.d.mts +22 -1
- package/dist/view-compiler.mjs +31 -1
- package/package.json +106 -23
|
@@ -0,0 +1,272 @@
|
|
|
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
|
+
};
|
|
38
|
+
declare const filterValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
|
|
39
|
+
eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
40
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
41
|
+
ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
42
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
43
|
+
in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
44
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
45
|
+
notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
46
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
47
|
+
isNull: z.ZodLiteral<true>;
|
|
48
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
49
|
+
isNotNull: z.ZodLiteral<true>;
|
|
50
|
+
}, z.core.$strict>]>]>;
|
|
51
|
+
/** `$raw` is a reserved key; a column literally named `$raw` is not addressable. */
|
|
52
|
+
declare const filterSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
53
|
+
$raw: z.ZodString;
|
|
54
|
+
}, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
|
|
55
|
+
eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
56
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
57
|
+
ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
58
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
59
|
+
in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
60
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
61
|
+
notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
62
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
63
|
+
isNull: z.ZodLiteral<true>;
|
|
64
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
65
|
+
isNotNull: z.ZodLiteral<true>;
|
|
66
|
+
}, z.core.$strict>]>]>>]>;
|
|
67
|
+
type FilterExpr = z.infer<typeof filterSchema>;
|
|
68
|
+
/** A single column predicate value (scalar shorthand or operator object). */
|
|
69
|
+
type FilterValue = z.infer<typeof filterValueSchema>;
|
|
70
|
+
/**
|
|
71
|
+
* How the link between two entities is expressed in the host schema:
|
|
72
|
+
* - `fkOn: "company"` — FK lives on the company table (e.g. organizations.owner_id)
|
|
73
|
+
* - `fkOn: "contact"` — FK lives on the contact table (e.g. users.organization_id)
|
|
74
|
+
* - `through` — a join table sits between them (many-to-many, role often here)
|
|
75
|
+
* - `$raw` — opaque SQL join fragment (escape hatch; not schema-validated)
|
|
76
|
+
*
|
|
77
|
+
* `column` and the join-table keys are `table.column` or bare `column` strings.
|
|
78
|
+
*/
|
|
79
|
+
declare const relationshipViaSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
80
|
+
fkOn: z.ZodEnum<{
|
|
81
|
+
contact: "contact";
|
|
82
|
+
company: "company";
|
|
83
|
+
}>;
|
|
84
|
+
column: z.ZodString;
|
|
85
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
86
|
+
through: z.ZodString;
|
|
87
|
+
localKey: z.ZodString;
|
|
88
|
+
foreignKey: z.ZodString;
|
|
89
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
90
|
+
$raw: z.ZodString;
|
|
91
|
+
}, z.core.$strict>]>;
|
|
92
|
+
type RelationshipVia = z.infer<typeof relationshipViaSchema>;
|
|
93
|
+
declare const relationshipSchema: z.ZodObject<{
|
|
94
|
+
entity: z.ZodString;
|
|
95
|
+
via: z.ZodUnion<readonly [z.ZodObject<{
|
|
96
|
+
fkOn: z.ZodEnum<{
|
|
97
|
+
contact: "contact";
|
|
98
|
+
company: "company";
|
|
99
|
+
}>;
|
|
100
|
+
column: z.ZodString;
|
|
101
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
102
|
+
through: z.ZodString;
|
|
103
|
+
localKey: z.ZodString;
|
|
104
|
+
foreignKey: z.ZodString;
|
|
105
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
106
|
+
$raw: z.ZodString;
|
|
107
|
+
}, z.core.$strict>]>;
|
|
108
|
+
}, z.core.$strict>;
|
|
109
|
+
type RelationshipSpec = z.infer<typeof relationshipSchema>;
|
|
110
|
+
/** A named map of relationships, as stored on an entity / in the mappings row. */
|
|
111
|
+
declare const relationshipsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
112
|
+
entity: z.ZodString;
|
|
113
|
+
via: z.ZodUnion<readonly [z.ZodObject<{
|
|
114
|
+
fkOn: z.ZodEnum<{
|
|
115
|
+
contact: "contact";
|
|
116
|
+
company: "company";
|
|
117
|
+
}>;
|
|
118
|
+
column: z.ZodString;
|
|
119
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
120
|
+
through: z.ZodString;
|
|
121
|
+
localKey: z.ZodString;
|
|
122
|
+
foreignKey: z.ZodString;
|
|
123
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
124
|
+
$raw: z.ZodString;
|
|
125
|
+
}, z.core.$strict>]>;
|
|
126
|
+
}, z.core.$strict>>;
|
|
127
|
+
type RelationshipsMap = z.infer<typeof relationshipsSchema>;
|
|
128
|
+
declare const entitySchema: z.ZodObject<{
|
|
129
|
+
concept: z.ZodEnum<{
|
|
130
|
+
contact: "contact";
|
|
131
|
+
company: "company";
|
|
132
|
+
deal: "deal";
|
|
133
|
+
activity: "activity";
|
|
134
|
+
conversation: "conversation";
|
|
135
|
+
note: "note";
|
|
136
|
+
}>;
|
|
137
|
+
source: z.ZodString;
|
|
138
|
+
role: z.ZodOptional<z.ZodString>;
|
|
139
|
+
filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
140
|
+
$raw: z.ZodString;
|
|
141
|
+
}, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
|
|
142
|
+
eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
143
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
144
|
+
ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
145
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
146
|
+
in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
147
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
148
|
+
notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
149
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
150
|
+
isNull: z.ZodLiteral<true>;
|
|
151
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
152
|
+
isNotNull: z.ZodLiteral<true>;
|
|
153
|
+
}, z.core.$strict>]>]>>]>>;
|
|
154
|
+
fields: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
155
|
+
readOnly: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
156
|
+
relationships: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
157
|
+
entity: z.ZodString;
|
|
158
|
+
via: z.ZodUnion<readonly [z.ZodObject<{
|
|
159
|
+
fkOn: z.ZodEnum<{
|
|
160
|
+
contact: "contact";
|
|
161
|
+
company: "company";
|
|
162
|
+
}>;
|
|
163
|
+
column: z.ZodString;
|
|
164
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
165
|
+
through: z.ZodString;
|
|
166
|
+
localKey: z.ZodString;
|
|
167
|
+
foreignKey: z.ZodString;
|
|
168
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
169
|
+
$raw: z.ZodString;
|
|
170
|
+
}, z.core.$strict>]>;
|
|
171
|
+
}, z.core.$strict>>>;
|
|
172
|
+
}, z.core.$strict>;
|
|
173
|
+
type ClivlyEntityConfig = z.infer<typeof entitySchema>;
|
|
174
|
+
declare const entitiesConfigSchema: z.ZodObject<{
|
|
175
|
+
entities: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
176
|
+
concept: z.ZodEnum<{
|
|
177
|
+
contact: "contact";
|
|
178
|
+
company: "company";
|
|
179
|
+
deal: "deal";
|
|
180
|
+
activity: "activity";
|
|
181
|
+
conversation: "conversation";
|
|
182
|
+
note: "note";
|
|
183
|
+
}>;
|
|
184
|
+
source: z.ZodString;
|
|
185
|
+
role: z.ZodOptional<z.ZodString>;
|
|
186
|
+
filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
187
|
+
$raw: z.ZodString;
|
|
188
|
+
}, z.core.$strict>, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
|
|
189
|
+
eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
190
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
191
|
+
ne: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
192
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
193
|
+
in: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
194
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
195
|
+
notIn: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
196
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
197
|
+
isNull: z.ZodLiteral<true>;
|
|
198
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
199
|
+
isNotNull: z.ZodLiteral<true>;
|
|
200
|
+
}, z.core.$strict>]>]>>]>>;
|
|
201
|
+
fields: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
202
|
+
readOnly: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
203
|
+
relationships: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
204
|
+
entity: z.ZodString;
|
|
205
|
+
via: z.ZodUnion<readonly [z.ZodObject<{
|
|
206
|
+
fkOn: z.ZodEnum<{
|
|
207
|
+
contact: "contact";
|
|
208
|
+
company: "company";
|
|
209
|
+
}>;
|
|
210
|
+
column: z.ZodString;
|
|
211
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
212
|
+
through: z.ZodString;
|
|
213
|
+
localKey: z.ZodString;
|
|
214
|
+
foreignKey: z.ZodString;
|
|
215
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
216
|
+
$raw: z.ZodString;
|
|
217
|
+
}, z.core.$strict>]>;
|
|
218
|
+
}, z.core.$strict>>>;
|
|
219
|
+
}, z.core.$strict>>;
|
|
220
|
+
}, z.core.$strict>;
|
|
221
|
+
type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
|
|
222
|
+
/** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
|
|
223
|
+
declare class ClivlyConfigError extends Error {
|
|
224
|
+
readonly issues: ConfigValidationError[];
|
|
225
|
+
constructor(issues: ConfigValidationError[]);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Structural helper for `clivly.config.ts`. Gives full type inference on the
|
|
229
|
+
* literal you pass and eagerly parses it, so shape errors surface at config
|
|
230
|
+
* load rather than at sync time.
|
|
231
|
+
*
|
|
232
|
+
* It also catches the silent-data-loss footgun: a non-canonical field key for a
|
|
233
|
+
* materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
|
|
234
|
+
* destination column, so the store would write an empty value. By default these
|
|
235
|
+
* are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
|
|
236
|
+
* a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
|
|
237
|
+
* needs the discovered schema; see `validateEntitiesConfig`.
|
|
238
|
+
*/
|
|
239
|
+
declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
|
|
240
|
+
strict?: boolean;
|
|
241
|
+
onWarn?: (message: string) => void;
|
|
242
|
+
}): ClivlyEntitiesConfig;
|
|
243
|
+
/**
|
|
244
|
+
* A host table the SDK reported for discovery. Structurally identical to the
|
|
245
|
+
* SDK's `DiscoveredTable`; duplicated here to keep `@clivly/core` free of an
|
|
246
|
+
* SDK dependency (core is the base contract SDK builds on).
|
|
247
|
+
*/
|
|
248
|
+
interface DiscoveredSchemaTable {
|
|
249
|
+
columns: string[];
|
|
250
|
+
name: string;
|
|
251
|
+
}
|
|
252
|
+
interface ConfigValidationError {
|
|
253
|
+
message: string;
|
|
254
|
+
/** Dotted path into the config, e.g. `entities.owner.fields.email`. */
|
|
255
|
+
path: string;
|
|
256
|
+
}
|
|
257
|
+
interface ConfigValidationResult {
|
|
258
|
+
errors: ConfigValidationError[];
|
|
259
|
+
valid: boolean;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Validate a (structurally valid) config against the host's discovered schema:
|
|
263
|
+
* source tables exist, mapped columns exist, `readOnly` ⊆ `fields`, filter
|
|
264
|
+
* columns exist, and relationships point at real entities/tables/columns.
|
|
265
|
+
*
|
|
266
|
+
* `$raw` filters and `$raw` relationship joins are opaque and skipped. Returns
|
|
267
|
+
* every error found (not just the first) so a mapping UI or CLI can show them
|
|
268
|
+
* all at once.
|
|
269
|
+
*/
|
|
270
|
+
declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
|
|
271
|
+
//#endregion
|
|
272
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
package/dist/entity-config.d.mts
CHANGED
|
@@ -219,13 +219,27 @@ declare const entitiesConfigSchema: z.ZodObject<{
|
|
|
219
219
|
}, z.core.$strict>>;
|
|
220
220
|
}, z.core.$strict>;
|
|
221
221
|
type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
|
|
222
|
+
/** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
|
|
223
|
+
declare class ClivlyConfigError extends Error {
|
|
224
|
+
readonly issues: ConfigValidationError[];
|
|
225
|
+
constructor(issues: ConfigValidationError[]);
|
|
226
|
+
}
|
|
222
227
|
/**
|
|
223
228
|
* Structural helper for `clivly.config.ts`. Gives full type inference on the
|
|
224
229
|
* literal you pass and eagerly parses it, so shape errors surface at config
|
|
225
|
-
* load rather than at sync time.
|
|
226
|
-
*
|
|
230
|
+
* load rather than at sync time.
|
|
231
|
+
*
|
|
232
|
+
* It also catches the silent-data-loss footgun: a non-canonical field key for a
|
|
233
|
+
* materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
|
|
234
|
+
* destination column, so the store would write an empty value. By default these
|
|
235
|
+
* are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
|
|
236
|
+
* a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
|
|
237
|
+
* needs the discovered schema; see `validateEntitiesConfig`.
|
|
227
238
|
*/
|
|
228
|
-
declare function defineClivlyConfig(config: ClivlyEntitiesConfig
|
|
239
|
+
declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
|
|
240
|
+
strict?: boolean;
|
|
241
|
+
onWarn?: (message: string) => void;
|
|
242
|
+
}): ClivlyEntitiesConfig;
|
|
229
243
|
/**
|
|
230
244
|
* A host table the SDK reported for discovery. Structurally identical to the
|
|
231
245
|
* SDK's `DiscoveredTable`; duplicated here to keep `@clivly/core` free of an
|
|
@@ -255,4 +269,4 @@ interface ConfigValidationResult {
|
|
|
255
269
|
*/
|
|
256
270
|
declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
|
|
257
271
|
//#endregion
|
|
258
|
-
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
|
272
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
package/dist/entity-config.mjs
CHANGED
|
@@ -146,14 +146,56 @@ const entitySchema = z.strictObject({
|
|
|
146
146
|
relationships: z.record(z.string(), relationshipSchema).optional()
|
|
147
147
|
});
|
|
148
148
|
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" }) });
|
|
149
|
+
/** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
|
|
150
|
+
var ClivlyConfigError = class extends Error {
|
|
151
|
+
issues;
|
|
152
|
+
constructor(issues) {
|
|
153
|
+
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")}`);
|
|
154
|
+
this.name = "ClivlyConfigError";
|
|
155
|
+
this.issues = issues;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function nonCanonicalFieldKeys(entity) {
|
|
159
|
+
const canonical = CANONICAL_FIELDS[entity.concept];
|
|
160
|
+
if (!canonical) return [];
|
|
161
|
+
return Object.keys(entity.fields).filter((key) => !canonical.includes(key));
|
|
162
|
+
}
|
|
163
|
+
function nonCanonicalMessage(entity, field) {
|
|
164
|
+
const canonical = CANONICAL_FIELDS[entity.concept] ?? [];
|
|
165
|
+
return `"${field}" is not a canonical ${entity.concept} field (expected one of: ${canonical.join(", ")})`;
|
|
166
|
+
}
|
|
167
|
+
function canonicalFieldIssues(config) {
|
|
168
|
+
const issues = [];
|
|
169
|
+
for (const [key, entity] of Object.entries(config.entities)) for (const field of nonCanonicalFieldKeys(entity)) issues.push({
|
|
170
|
+
path: `entities.${key}.fields.${field}`,
|
|
171
|
+
message: nonCanonicalMessage(entity, field)
|
|
172
|
+
});
|
|
173
|
+
return issues;
|
|
174
|
+
}
|
|
175
|
+
function defaultWarn(message) {
|
|
176
|
+
console.warn(message);
|
|
177
|
+
}
|
|
149
178
|
/**
|
|
150
179
|
* Structural helper for `clivly.config.ts`. Gives full type inference on the
|
|
151
180
|
* literal you pass and eagerly parses it, so shape errors surface at config
|
|
152
|
-
* load rather than at sync time.
|
|
153
|
-
*
|
|
181
|
+
* load rather than at sync time.
|
|
182
|
+
*
|
|
183
|
+
* It also catches the silent-data-loss footgun: a non-canonical field key for a
|
|
184
|
+
* materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
|
|
185
|
+
* destination column, so the store would write an empty value. By default these
|
|
186
|
+
* are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
|
|
187
|
+
* a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
|
|
188
|
+
* needs the discovered schema; see `validateEntitiesConfig`.
|
|
154
189
|
*/
|
|
155
|
-
function defineClivlyConfig(config) {
|
|
156
|
-
|
|
190
|
+
function defineClivlyConfig(config, options) {
|
|
191
|
+
const parsed = entitiesConfigSchema.parse(config);
|
|
192
|
+
const issues = canonicalFieldIssues(parsed);
|
|
193
|
+
if (issues.length > 0) {
|
|
194
|
+
if (options?.strict) throw new ClivlyConfigError(issues);
|
|
195
|
+
const warn = options?.onWarn ?? defaultWarn;
|
|
196
|
+
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.`);
|
|
197
|
+
}
|
|
198
|
+
return parsed;
|
|
157
199
|
}
|
|
158
200
|
/** Split a `table.column` / bare `column` ref into its parts. */
|
|
159
201
|
function parseColumnRef(ref) {
|
|
@@ -191,11 +233,8 @@ var SchemaValidator = class {
|
|
|
191
233
|
}
|
|
192
234
|
};
|
|
193
235
|
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
|
-
}
|
|
236
|
+
for (const [field, column] of Object.entries(entity.fields)) v.requireColumn(`${base}.fields.${field}`, entity.source, column);
|
|
237
|
+
for (const field of nonCanonicalFieldKeys(entity)) v.report(`${base}.fields.${field}`, nonCanonicalMessage(entity, field));
|
|
199
238
|
}
|
|
200
239
|
function validateReadOnly(v, base, entity) {
|
|
201
240
|
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 +302,4 @@ function validateEntitiesConfig(config, schema) {
|
|
|
263
302
|
};
|
|
264
303
|
}
|
|
265
304
|
//#endregion
|
|
266
|
-
export { CANONICAL_FIELDS, CRM_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
|
305
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|