@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,431 @@
|
|
|
1
|
+
import { MATERIALIZING_CONCEPTS } from "./entity-config.mjs";
|
|
2
|
+
import { CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor } from "./mapping-score.mjs";
|
|
3
|
+
//#region src/entity-heuristics.ts
|
|
4
|
+
const TARGET_CONCEPTS = [
|
|
5
|
+
"contact",
|
|
6
|
+
"company",
|
|
7
|
+
"deal",
|
|
8
|
+
"activity",
|
|
9
|
+
"conversation",
|
|
10
|
+
"note"
|
|
11
|
+
];
|
|
12
|
+
const TABLE_PATTERNS = [
|
|
13
|
+
{
|
|
14
|
+
concept: "contact",
|
|
15
|
+
names: [
|
|
16
|
+
"users",
|
|
17
|
+
"customers",
|
|
18
|
+
"clients",
|
|
19
|
+
"accounts",
|
|
20
|
+
"participants",
|
|
21
|
+
"attendees",
|
|
22
|
+
"people",
|
|
23
|
+
"members",
|
|
24
|
+
"contacts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
concept: "company",
|
|
29
|
+
names: [
|
|
30
|
+
"companies",
|
|
31
|
+
"organizations",
|
|
32
|
+
"organisations",
|
|
33
|
+
"orgs",
|
|
34
|
+
"teams",
|
|
35
|
+
"businesses",
|
|
36
|
+
"accounts"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
concept: "deal",
|
|
41
|
+
names: [
|
|
42
|
+
"deals",
|
|
43
|
+
"opportunities",
|
|
44
|
+
"subscriptions",
|
|
45
|
+
"orders",
|
|
46
|
+
"invoices",
|
|
47
|
+
"sales"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
concept: "conversation",
|
|
52
|
+
names: [
|
|
53
|
+
"conversations",
|
|
54
|
+
"messages",
|
|
55
|
+
"tickets",
|
|
56
|
+
"threads",
|
|
57
|
+
"chats",
|
|
58
|
+
"inbox"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
concept: "activity",
|
|
63
|
+
names: [
|
|
64
|
+
"events",
|
|
65
|
+
"activities",
|
|
66
|
+
"jobs",
|
|
67
|
+
"logs",
|
|
68
|
+
"audit",
|
|
69
|
+
"shifts",
|
|
70
|
+
"scans",
|
|
71
|
+
"sessions"
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
concept: "note",
|
|
76
|
+
names: [
|
|
77
|
+
"notes",
|
|
78
|
+
"comments",
|
|
79
|
+
"annotations"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
const CONCEPT_FIELDS = {
|
|
84
|
+
contact: [{
|
|
85
|
+
field: "name",
|
|
86
|
+
candidates: [
|
|
87
|
+
"name",
|
|
88
|
+
"full_name",
|
|
89
|
+
"fullname",
|
|
90
|
+
"display_name"
|
|
91
|
+
],
|
|
92
|
+
required: false
|
|
93
|
+
}, {
|
|
94
|
+
field: "email",
|
|
95
|
+
candidates: ["email", "email_address"],
|
|
96
|
+
required: true
|
|
97
|
+
}],
|
|
98
|
+
company: [{
|
|
99
|
+
field: "name",
|
|
100
|
+
candidates: [
|
|
101
|
+
"name",
|
|
102
|
+
"company_name",
|
|
103
|
+
"legal_name",
|
|
104
|
+
"title"
|
|
105
|
+
],
|
|
106
|
+
required: true
|
|
107
|
+
}, {
|
|
108
|
+
field: "domain",
|
|
109
|
+
candidates: [
|
|
110
|
+
"domain",
|
|
111
|
+
"website",
|
|
112
|
+
"url"
|
|
113
|
+
],
|
|
114
|
+
required: false
|
|
115
|
+
}],
|
|
116
|
+
deal: [
|
|
117
|
+
{
|
|
118
|
+
field: "name",
|
|
119
|
+
candidates: ["name", "title"],
|
|
120
|
+
required: true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
field: "value",
|
|
124
|
+
candidates: [
|
|
125
|
+
"value",
|
|
126
|
+
"amount",
|
|
127
|
+
"total",
|
|
128
|
+
"price"
|
|
129
|
+
],
|
|
130
|
+
required: false
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
field: "stage",
|
|
134
|
+
candidates: [
|
|
135
|
+
"stage",
|
|
136
|
+
"status",
|
|
137
|
+
"state"
|
|
138
|
+
],
|
|
139
|
+
required: false
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
activity: [
|
|
143
|
+
{
|
|
144
|
+
field: "type",
|
|
145
|
+
candidates: [
|
|
146
|
+
"type",
|
|
147
|
+
"kind",
|
|
148
|
+
"event_type"
|
|
149
|
+
],
|
|
150
|
+
required: false
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
field: "summary",
|
|
154
|
+
candidates: [
|
|
155
|
+
"summary",
|
|
156
|
+
"description",
|
|
157
|
+
"message",
|
|
158
|
+
"title"
|
|
159
|
+
],
|
|
160
|
+
required: false
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
field: "createdAt",
|
|
164
|
+
candidates: [
|
|
165
|
+
"created_at",
|
|
166
|
+
"createdat",
|
|
167
|
+
"timestamp"
|
|
168
|
+
],
|
|
169
|
+
required: false
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
conversation: [{
|
|
173
|
+
field: "status",
|
|
174
|
+
candidates: ["status", "state"],
|
|
175
|
+
required: false
|
|
176
|
+
}, {
|
|
177
|
+
field: "subject",
|
|
178
|
+
candidates: [
|
|
179
|
+
"subject",
|
|
180
|
+
"title",
|
|
181
|
+
"preview"
|
|
182
|
+
],
|
|
183
|
+
required: false
|
|
184
|
+
}],
|
|
185
|
+
note: [{
|
|
186
|
+
field: "body",
|
|
187
|
+
candidates: [
|
|
188
|
+
"body",
|
|
189
|
+
"content",
|
|
190
|
+
"text",
|
|
191
|
+
"note"
|
|
192
|
+
],
|
|
193
|
+
required: true
|
|
194
|
+
}]
|
|
195
|
+
};
|
|
196
|
+
const CURSOR_CANDIDATES = [
|
|
197
|
+
"updatedat",
|
|
198
|
+
"updated_at",
|
|
199
|
+
"modifiedat",
|
|
200
|
+
"modified_at",
|
|
201
|
+
"createdat",
|
|
202
|
+
"created_at"
|
|
203
|
+
];
|
|
204
|
+
function suggestCursorField(columnKeys) {
|
|
205
|
+
const lowerToActual = new Map(columnKeys.map((c) => [c.toLowerCase(), c]));
|
|
206
|
+
for (const candidate of CURSOR_CANDIDATES) {
|
|
207
|
+
const match = lowerToActual.get(candidate);
|
|
208
|
+
if (match) return match;
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const MATERIALIZING = new Set(MATERIALIZING_CONCEPTS);
|
|
213
|
+
function matchesConcept(tableName, concept) {
|
|
214
|
+
const normalized = tableName.toLowerCase();
|
|
215
|
+
const entry = TABLE_PATTERNS.find((p) => p.concept === concept);
|
|
216
|
+
if (!entry) return null;
|
|
217
|
+
if (entry.names.includes(normalized)) return { exact: true };
|
|
218
|
+
if (entry.names.some((name) => normalized.includes(name))) return { exact: false };
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
function metaFor(table, column) {
|
|
222
|
+
return table.columnsMeta?.find((c) => c.name === column);
|
|
223
|
+
}
|
|
224
|
+
function hasDefiningColumn(concept, columns) {
|
|
225
|
+
const lower = new Set(columns.map((c) => c.toLowerCase()));
|
|
226
|
+
const required = CONCEPT_FIELDS[concept].filter((f) => f.required);
|
|
227
|
+
return required.length > 0 && required.every((f) => f.candidates.some((c) => lower.has(c)));
|
|
228
|
+
}
|
|
229
|
+
function scoreConcepts(table) {
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const concept of TARGET_CONCEPTS) {
|
|
232
|
+
if (!MATERIALIZING.has(concept)) continue;
|
|
233
|
+
const nameMatch = matchesConcept(table.name, concept);
|
|
234
|
+
if (!nameMatch) continue;
|
|
235
|
+
let score = 0;
|
|
236
|
+
const reasons = [];
|
|
237
|
+
if (nameMatch.exact) {
|
|
238
|
+
score += W.nameExact;
|
|
239
|
+
reasons.push(`name matches "${table.name}"`);
|
|
240
|
+
} else {
|
|
241
|
+
score += W.nameSubstring;
|
|
242
|
+
reasons.push(`name looks like ${concept}`);
|
|
243
|
+
}
|
|
244
|
+
if (hasDefiningColumn(concept, table.columns)) {
|
|
245
|
+
score += W.definingColumn;
|
|
246
|
+
reasons.push(`has the columns a ${concept} needs`);
|
|
247
|
+
}
|
|
248
|
+
if (table.columnsMeta?.some((c) => c.isPrimaryKey)) {
|
|
249
|
+
score += W.hasPrimaryKey;
|
|
250
|
+
reasons.push("has primary key");
|
|
251
|
+
}
|
|
252
|
+
if (table.rowCount !== void 0) if (table.rowCount === 0) {
|
|
253
|
+
score += W.emptyTable;
|
|
254
|
+
reasons.push("table is empty");
|
|
255
|
+
} else {
|
|
256
|
+
score += W.hasRows;
|
|
257
|
+
reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
|
|
258
|
+
}
|
|
259
|
+
out.push({
|
|
260
|
+
value: concept,
|
|
261
|
+
score,
|
|
262
|
+
tier: tierFor(score, CONCEPT_TIERS),
|
|
263
|
+
reasons
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return out.sort((a, b) => b.score - a.score);
|
|
267
|
+
}
|
|
268
|
+
function topConcept(table) {
|
|
269
|
+
const ranked = scoreConcepts(table);
|
|
270
|
+
const best = ranked[0];
|
|
271
|
+
if (!best) return null;
|
|
272
|
+
return {
|
|
273
|
+
best,
|
|
274
|
+
ambiguous: isAmbiguous(ranked)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function requiredFieldsFor(concept) {
|
|
278
|
+
if (concept === "custom") return [];
|
|
279
|
+
return CONCEPT_FIELDS[concept].filter((f) => f.required).map((f) => f.field);
|
|
280
|
+
}
|
|
281
|
+
function validateMapping(requiredFields, fieldMap) {
|
|
282
|
+
const missing = requiredFields.filter((field) => !fieldMap[field]);
|
|
283
|
+
return {
|
|
284
|
+
valid: missing.length === 0,
|
|
285
|
+
missing
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const DISCRIMINATOR_COLUMNS = [
|
|
289
|
+
"role",
|
|
290
|
+
"type",
|
|
291
|
+
"kind",
|
|
292
|
+
"status",
|
|
293
|
+
"state",
|
|
294
|
+
"category",
|
|
295
|
+
"tier"
|
|
296
|
+
];
|
|
297
|
+
function suggestDiscriminatorColumns(columns) {
|
|
298
|
+
const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
|
|
299
|
+
const out = [];
|
|
300
|
+
for (const name of DISCRIMINATOR_COLUMNS) {
|
|
301
|
+
const match = lowerToActual.get(name);
|
|
302
|
+
if (match) out.push(match);
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
const COMPANY_FK_HINTS = [
|
|
307
|
+
"organization_id",
|
|
308
|
+
"organisation_id",
|
|
309
|
+
"org_id",
|
|
310
|
+
"company_id",
|
|
311
|
+
"account_id",
|
|
312
|
+
"team_id"
|
|
313
|
+
];
|
|
314
|
+
function firstMatchingColumn(columns, hints) {
|
|
315
|
+
const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
|
|
316
|
+
for (const hint of hints) {
|
|
317
|
+
const match = lowerToActual.get(hint);
|
|
318
|
+
if (match) return match;
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Suggest contact→company relationships. When the source table carries FK
|
|
324
|
+
* metadata (`references`), the target table is a *fact*, not a name guess —
|
|
325
|
+
* that scores highest. Without metadata (v1 hosts) we fall back to the legacy
|
|
326
|
+
* FK-name hints at a lower tier.
|
|
327
|
+
*/
|
|
328
|
+
function scoreRelationships(source, allTables) {
|
|
329
|
+
const companyTables = allTables.filter((t) => t.name !== source.name && topConcept(t)?.best.value === "company");
|
|
330
|
+
if (companyTables.length === 0) return [];
|
|
331
|
+
const companyNames = new Set(companyTables.map((t) => t.name.toLowerCase()));
|
|
332
|
+
const out = [];
|
|
333
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
334
|
+
for (const col of source.columnsMeta ?? []) {
|
|
335
|
+
const ref = col.references;
|
|
336
|
+
if (!(ref && companyNames.has(ref.table.toLowerCase()))) continue;
|
|
337
|
+
if (seenTargets.has(ref.table.toLowerCase())) continue;
|
|
338
|
+
seenTargets.add(ref.table.toLowerCase());
|
|
339
|
+
out.push({
|
|
340
|
+
value: {
|
|
341
|
+
name: "company",
|
|
342
|
+
entityTable: ref.table,
|
|
343
|
+
via: {
|
|
344
|
+
fkOn: "contact",
|
|
345
|
+
column: `${source.name}.${col.name}`
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
score: W.fkResolved,
|
|
349
|
+
tier: tierFor(W.fkResolved, RELATIONSHIP_TIERS),
|
|
350
|
+
reasons: [`foreign key → ${ref.table}.${ref.column}`]
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (source.columnsMeta?.length) return out.sort((a, b) => b.score - a.score);
|
|
354
|
+
const contactFk = firstMatchingColumn(source.columns, COMPANY_FK_HINTS);
|
|
355
|
+
if (contactFk) out.push({
|
|
356
|
+
value: {
|
|
357
|
+
name: "company",
|
|
358
|
+
entityTable: companyTables[0]?.name ?? "",
|
|
359
|
+
via: {
|
|
360
|
+
fkOn: "contact",
|
|
361
|
+
column: `${source.name}.${contactFk}`
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
score: W.fkNameHint,
|
|
365
|
+
tier: tierFor(W.fkNameHint, RELATIONSHIP_TIERS),
|
|
366
|
+
reasons: [`column "${contactFk}" looks like a company link`]
|
|
367
|
+
});
|
|
368
|
+
return out.sort((a, b) => b.score - a.score);
|
|
369
|
+
}
|
|
370
|
+
const FIELD_TYPE_EXPECTATION = {
|
|
371
|
+
name: "text",
|
|
372
|
+
email: "text",
|
|
373
|
+
domain: "text",
|
|
374
|
+
stage: "text",
|
|
375
|
+
status: "text",
|
|
376
|
+
subject: "text",
|
|
377
|
+
body: "text",
|
|
378
|
+
summary: "text",
|
|
379
|
+
type: "text",
|
|
380
|
+
value: "numeric",
|
|
381
|
+
createdAt: "timestamp"
|
|
382
|
+
};
|
|
383
|
+
function typeFits(field, type) {
|
|
384
|
+
const expectation = FIELD_TYPE_EXPECTATION[field];
|
|
385
|
+
if (!(expectation && type)) return null;
|
|
386
|
+
if (expectation === "text") return isTextLike(type);
|
|
387
|
+
if (expectation === "numeric") return isNumericLike(type);
|
|
388
|
+
return isTimestampLike(type);
|
|
389
|
+
}
|
|
390
|
+
function scoreFieldMap(concept, table) {
|
|
391
|
+
const lowerToActual = new Map(table.columns.map((c) => [c.toLowerCase(), c]));
|
|
392
|
+
const out = [];
|
|
393
|
+
for (const { field, candidates, required } of CONCEPT_FIELDS[concept]) for (const candidate of candidates) {
|
|
394
|
+
const column = lowerToActual.get(candidate);
|
|
395
|
+
if (!column) continue;
|
|
396
|
+
let score = W.fieldNameMatch;
|
|
397
|
+
const reasons = [`name matches "${column}"`];
|
|
398
|
+
const meta = metaFor(table, column);
|
|
399
|
+
const fits = typeFits(field, meta?.type);
|
|
400
|
+
if (fits === true) {
|
|
401
|
+
score += W.typeCompatible;
|
|
402
|
+
reasons.push(`type ${meta?.type}`);
|
|
403
|
+
} else if (fits === false) {
|
|
404
|
+
score += W.typeMismatch;
|
|
405
|
+
reasons.push(`type ${meta?.type} does not fit ${field}`);
|
|
406
|
+
}
|
|
407
|
+
if (required && meta?.nullable === false) {
|
|
408
|
+
score += W.requiredNotNull;
|
|
409
|
+
reasons.push("not null");
|
|
410
|
+
}
|
|
411
|
+
out.push({
|
|
412
|
+
value: {
|
|
413
|
+
field,
|
|
414
|
+
column,
|
|
415
|
+
required
|
|
416
|
+
},
|
|
417
|
+
score,
|
|
418
|
+
tier: tierFor(score, FIELD_TIERS),
|
|
419
|
+
reasons
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
function toFieldMap(scored) {
|
|
426
|
+
const map = {};
|
|
427
|
+
for (const { value } of scored) map[value.field] = value.column;
|
|
428
|
+
return map;
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
export { TARGET_CONCEPTS, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_auth_adapter = require("./auth-adapter.cjs");
|
|
3
|
+
const require_entity_config = require("./entity-config.cjs");
|
|
4
|
+
const require_mapping_form = require("./mapping-form.cjs");
|
|
5
|
+
const require_mappings_config = require("./mappings-config.cjs");
|
|
6
|
+
const require_view_compiler = require("./view-compiler.cjs");
|
|
7
|
+
const require_sync_engine = require("./sync-engine.cjs");
|
|
8
|
+
exports.CANONICAL_FIELDS = require_entity_config.CANONICAL_FIELDS;
|
|
9
|
+
exports.CRM_CONCEPTS = require_entity_config.CRM_CONCEPTS;
|
|
10
|
+
exports.ClivlyConfigError = require_entity_config.ClivlyConfigError;
|
|
11
|
+
exports.MATERIALIZING_CONCEPTS = require_entity_config.MATERIALIZING_CONCEPTS;
|
|
12
|
+
exports.authCustom = require_auth_adapter.authCustom;
|
|
13
|
+
exports.buildFilterFromDiscriminator = require_mapping_form.buildFilterFromDiscriminator;
|
|
14
|
+
exports.buildRelationships = require_mapping_form.buildRelationships;
|
|
15
|
+
exports.compileEntityView = require_view_compiler.compileEntityView;
|
|
16
|
+
exports.compileEntityViews = require_view_compiler.compileEntityViews;
|
|
17
|
+
exports.defineClivlyConfig = require_entity_config.defineClivlyConfig;
|
|
18
|
+
exports.entitiesConfigSchema = require_entity_config.entitiesConfigSchema;
|
|
19
|
+
exports.explainEntitiesConfig = require_view_compiler.explainEntitiesConfig;
|
|
20
|
+
exports.filterSchema = require_entity_config.filterSchema;
|
|
21
|
+
exports.mappingsToEntitiesConfig = require_mappings_config.mappingsToEntitiesConfig;
|
|
22
|
+
exports.reconcile = require_sync_engine.reconcile;
|
|
23
|
+
exports.relationshipSchema = require_entity_config.relationshipSchema;
|
|
24
|
+
exports.relationshipViaSignature = require_mapping_form.relationshipViaSignature;
|
|
25
|
+
exports.relationshipsSchema = require_entity_config.relationshipsSchema;
|
|
26
|
+
exports.runSync = require_sync_engine.runSync;
|
|
27
|
+
exports.validateEntitiesConfig = require_entity_config.validateEntitiesConfig;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, RecordSource, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.cjs";
|
|
2
|
+
import { CRMAdapter } from "./adapter.cjs";
|
|
3
|
+
import { ClivlyAuthAdapter, ClivlyUser, CrmMembership, MembershipResolver, authCustom } from "./auth-adapter.cjs";
|
|
4
|
+
import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.cjs";
|
|
5
|
+
import { RelationshipCandidate, buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.cjs";
|
|
6
|
+
import { EntityMappingRow, mappingsToEntitiesConfig } from "./mappings-config.cjs";
|
|
7
|
+
import { CompileOptions, CompiledView, ExplainResult, compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.cjs";
|
|
8
|
+
import { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync } from "./sync-engine.cjs";
|
|
9
|
+
export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type ExplainResult, type FilterExpr, type FilterValue, MATERIALIZING_CONCEPTS, type MaterializingConcept, type MembershipResolver, type MirrorRecord, Note, type PersistAction, RecordSource, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateTaskInput } from "./types.mjs";
|
|
1
|
+
import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, RecordSource, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.mjs";
|
|
2
2
|
import { CRMAdapter } from "./adapter.mjs";
|
|
3
3
|
import { ClivlyAuthAdapter, ClivlyUser, CrmMembership, MembershipResolver, authCustom } from "./auth-adapter.mjs";
|
|
4
|
-
import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
|
|
4
|
+
import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
|
|
5
5
|
import { RelationshipCandidate, buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
|
|
6
6
|
import { EntityMappingRow, mappingsToEntitiesConfig } from "./mappings-config.mjs";
|
|
7
|
-
import { CompileOptions, CompiledView, compileEntityView, compileEntityViews } from "./view-compiler.mjs";
|
|
7
|
+
import { CompileOptions, CompiledView, ExplainResult, compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.mjs";
|
|
8
8
|
import { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync } from "./sync-engine.mjs";
|
|
9
|
-
export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type FilterExpr, type FilterValue, type MembershipResolver, type MirrorRecord, Note, type PersistAction, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
|
|
9
|
+
export { Activity, ActivityType, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, Company, type CompileOptions, type CompiledView, type ConfigValidationError, type ConfigValidationResult, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, type CrmConcept, type CrmMembership, CrmRole, Deal, DealStage, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type ExplainResult, type FilterExpr, type FilterValue, MATERIALIZING_CONCEPTS, type MaterializingConcept, type MembershipResolver, type MirrorRecord, Note, type PersistAction, RecordSource, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SyncAction, type SyncResult, type SyncStore, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authCustom } from "./auth-adapter.mjs";
|
|
2
|
-
import { CANONICAL_FIELDS, CRM_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
|
|
2
|
+
import { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, MATERIALIZING_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig } from "./entity-config.mjs";
|
|
3
3
|
import { buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
|
|
4
4
|
import { mappingsToEntitiesConfig } from "./mappings-config.mjs";
|
|
5
|
-
import { compileEntityView, compileEntityViews } from "./view-compiler.mjs";
|
|
5
|
+
import { compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.mjs";
|
|
6
6
|
import { reconcile, runSync } from "./sync-engine.mjs";
|
|
7
|
-
export { CANONICAL_FIELDS, CRM_CONCEPTS, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
|
|
7
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, MATERIALIZING_CONCEPTS, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, mappingsToEntitiesConfig, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, runSync, validateEntitiesConfig };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/mapping-form.ts
|
|
3
|
+
/**
|
|
4
|
+
* Build the filter for a discriminated entity: rows where `column` equals
|
|
5
|
+
* `value` belong to this entity (e.g. `role = "owner"`). Returns `undefined`
|
|
6
|
+
* when either side is blank so the caller can omit the filter entirely.
|
|
7
|
+
*/
|
|
8
|
+
function buildFilterFromDiscriminator(column, value) {
|
|
9
|
+
const col = column?.trim();
|
|
10
|
+
const val = value.trim();
|
|
11
|
+
if (!col || val === "") return;
|
|
12
|
+
return { [col]: { eq: val } };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A stable identity string for a relationship's `via`. Two candidates on the
|
|
16
|
+
* same source table always differ in their join (`fkOn`+column, join-table
|
|
17
|
+
* keys, or raw SQL), so this disambiguates candidates the heuristic gives the
|
|
18
|
+
* same `name` (every contact→company candidate is named "company").
|
|
19
|
+
*/
|
|
20
|
+
function relationshipViaSignature(via) {
|
|
21
|
+
if ("fkOn" in via) return `fk:${via.fkOn}:${via.column}`;
|
|
22
|
+
if ("through" in via) return `through:${via.through}:${via.localKey}:${via.foreignKey}`;
|
|
23
|
+
return `raw:${via.$raw}`;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compile the selected relationship candidates into the persisted
|
|
27
|
+
* `RelationshipsMap`. Returns `undefined` for an empty selection so the caller
|
|
28
|
+
* omits the field.
|
|
29
|
+
*/
|
|
30
|
+
function buildRelationships(candidates) {
|
|
31
|
+
if (candidates.length === 0) return;
|
|
32
|
+
const map = {};
|
|
33
|
+
for (const candidate of candidates) {
|
|
34
|
+
let key = candidate.name;
|
|
35
|
+
let suffix = 2;
|
|
36
|
+
while (key in map) {
|
|
37
|
+
key = `${candidate.name}_${suffix}`;
|
|
38
|
+
suffix++;
|
|
39
|
+
}
|
|
40
|
+
map[key] = {
|
|
41
|
+
entity: candidate.entityTable,
|
|
42
|
+
via: candidate.via
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return map;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
exports.buildFilterFromDiscriminator = buildFilterFromDiscriminator;
|
|
49
|
+
exports.buildRelationships = buildRelationships;
|
|
50
|
+
exports.relationshipViaSignature = relationshipViaSignature;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FilterExpr, RelationshipVia, RelationshipsMap } from "./entity-config.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/mapping-form.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A relationship the mapping UI can offer or has configured, in the shape the
|
|
6
|
+
* heuristic suggester produces. `entityTable` is the *host table name*; the
|
|
7
|
+
* mappings→config compiler resolves it to the canonical entity key, so it is
|
|
8
|
+
* persisted verbatim as the relationship's `entity` reference.
|
|
9
|
+
*/
|
|
10
|
+
interface RelationshipCandidate {
|
|
11
|
+
entityTable: string;
|
|
12
|
+
name: string;
|
|
13
|
+
via: RelationshipVia;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the filter for a discriminated entity: rows where `column` equals
|
|
17
|
+
* `value` belong to this entity (e.g. `role = "owner"`). Returns `undefined`
|
|
18
|
+
* when either side is blank so the caller can omit the filter entirely.
|
|
19
|
+
*/
|
|
20
|
+
declare function buildFilterFromDiscriminator(column: string | null | undefined, value: string): FilterExpr | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* A stable identity string for a relationship's `via`. Two candidates on the
|
|
23
|
+
* same source table always differ in their join (`fkOn`+column, join-table
|
|
24
|
+
* keys, or raw SQL), so this disambiguates candidates the heuristic gives the
|
|
25
|
+
* same `name` (every contact→company candidate is named "company").
|
|
26
|
+
*/
|
|
27
|
+
declare function relationshipViaSignature(via: RelationshipVia): string;
|
|
28
|
+
/**
|
|
29
|
+
* Compile the selected relationship candidates into the persisted
|
|
30
|
+
* `RelationshipsMap`. Returns `undefined` for an empty selection so the caller
|
|
31
|
+
* omits the field.
|
|
32
|
+
*/
|
|
33
|
+
declare function buildRelationships(candidates: RelationshipCandidate[]): RelationshipsMap | undefined;
|
|
34
|
+
//#endregion
|
|
35
|
+
export { RelationshipCandidate, buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/mapping-score.ts
|
|
3
|
+
const CONCEPT_TIERS = {
|
|
4
|
+
high: 70,
|
|
5
|
+
medium: 40
|
|
6
|
+
};
|
|
7
|
+
const FIELD_TIERS = {
|
|
8
|
+
high: 70,
|
|
9
|
+
medium: 40
|
|
10
|
+
};
|
|
11
|
+
const RELATIONSHIP_TIERS = {
|
|
12
|
+
high: 60,
|
|
13
|
+
medium: 30
|
|
14
|
+
};
|
|
15
|
+
/** Two concept candidates closer than this are treated as ambiguous. */
|
|
16
|
+
const AMBIGUITY_MARGIN = 15;
|
|
17
|
+
/** Signal weights. Tuned against fixtures; adjust here, not at call sites. */
|
|
18
|
+
const W = {
|
|
19
|
+
nameExact: 50,
|
|
20
|
+
nameSubstring: 25,
|
|
21
|
+
definingColumn: 20,
|
|
22
|
+
hasPrimaryKey: 10,
|
|
23
|
+
hasRows: 10,
|
|
24
|
+
emptyTable: -20,
|
|
25
|
+
fieldNameMatch: 50,
|
|
26
|
+
typeCompatible: 30,
|
|
27
|
+
typeMismatch: -30,
|
|
28
|
+
requiredNotNull: 10,
|
|
29
|
+
fkResolved: 60,
|
|
30
|
+
fkNameHint: 30,
|
|
31
|
+
joinTable: 20
|
|
32
|
+
};
|
|
33
|
+
function tierFor(score, t) {
|
|
34
|
+
if (score >= t.high) return "High";
|
|
35
|
+
if (score >= t.medium) return "Medium";
|
|
36
|
+
return "Low";
|
|
37
|
+
}
|
|
38
|
+
const TEXT_TYPES = [
|
|
39
|
+
"text",
|
|
40
|
+
"varchar",
|
|
41
|
+
"char",
|
|
42
|
+
"citext",
|
|
43
|
+
"uuid",
|
|
44
|
+
"string"
|
|
45
|
+
];
|
|
46
|
+
const NUMERIC_TYPES = [
|
|
47
|
+
"numeric",
|
|
48
|
+
"decimal",
|
|
49
|
+
"int",
|
|
50
|
+
"integer",
|
|
51
|
+
"bigint",
|
|
52
|
+
"smallint",
|
|
53
|
+
"float",
|
|
54
|
+
"double",
|
|
55
|
+
"real",
|
|
56
|
+
"money"
|
|
57
|
+
];
|
|
58
|
+
const TIMESTAMP_TYPES = [
|
|
59
|
+
"timestamp",
|
|
60
|
+
"timestamptz",
|
|
61
|
+
"date",
|
|
62
|
+
"datetime"
|
|
63
|
+
];
|
|
64
|
+
function matchesType(type, list) {
|
|
65
|
+
if (!type) return false;
|
|
66
|
+
const normalized = type.toLowerCase();
|
|
67
|
+
return list.some((t) => normalized.startsWith(t));
|
|
68
|
+
}
|
|
69
|
+
function isTextLike(type) {
|
|
70
|
+
return matchesType(type, TEXT_TYPES);
|
|
71
|
+
}
|
|
72
|
+
function isNumericLike(type) {
|
|
73
|
+
return matchesType(type, NUMERIC_TYPES);
|
|
74
|
+
}
|
|
75
|
+
function isTimestampLike(type) {
|
|
76
|
+
return matchesType(type, TIMESTAMP_TYPES);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* True when the top two candidates are too close to call. Callers must never
|
|
80
|
+
* one-click apply an ambiguous result — the user picks.
|
|
81
|
+
*/
|
|
82
|
+
function isAmbiguous(ranked) {
|
|
83
|
+
const [first, second] = ranked;
|
|
84
|
+
if (!(first && second)) return false;
|
|
85
|
+
return first.score - second.score < 15;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
exports.AMBIGUITY_MARGIN = AMBIGUITY_MARGIN;
|
|
89
|
+
exports.CONCEPT_TIERS = CONCEPT_TIERS;
|
|
90
|
+
exports.FIELD_TIERS = FIELD_TIERS;
|
|
91
|
+
exports.RELATIONSHIP_TIERS = RELATIONSHIP_TIERS;
|
|
92
|
+
exports.W = W;
|
|
93
|
+
exports.isAmbiguous = isAmbiguous;
|
|
94
|
+
exports.isNumericLike = isNumericLike;
|
|
95
|
+
exports.isTextLike = isTextLike;
|
|
96
|
+
exports.isTimestampLike = isTimestampLike;
|
|
97
|
+
exports.tierFor = tierFor;
|