@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,335 @@
|
|
|
1
|
+
//#region src/entity-heuristics.ts
|
|
2
|
+
const TARGET_CONCEPTS = [
|
|
3
|
+
"contact",
|
|
4
|
+
"company",
|
|
5
|
+
"deal",
|
|
6
|
+
"activity",
|
|
7
|
+
"conversation",
|
|
8
|
+
"note"
|
|
9
|
+
];
|
|
10
|
+
const TABLE_PATTERNS = [
|
|
11
|
+
{
|
|
12
|
+
concept: "contact",
|
|
13
|
+
names: [
|
|
14
|
+
"users",
|
|
15
|
+
"customers",
|
|
16
|
+
"clients",
|
|
17
|
+
"accounts",
|
|
18
|
+
"participants",
|
|
19
|
+
"attendees",
|
|
20
|
+
"people",
|
|
21
|
+
"members",
|
|
22
|
+
"contacts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
concept: "company",
|
|
27
|
+
names: [
|
|
28
|
+
"companies",
|
|
29
|
+
"organizations",
|
|
30
|
+
"organisations",
|
|
31
|
+
"orgs",
|
|
32
|
+
"teams",
|
|
33
|
+
"businesses",
|
|
34
|
+
"accounts"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
concept: "deal",
|
|
39
|
+
names: [
|
|
40
|
+
"deals",
|
|
41
|
+
"opportunities",
|
|
42
|
+
"subscriptions",
|
|
43
|
+
"orders",
|
|
44
|
+
"invoices",
|
|
45
|
+
"sales"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
concept: "conversation",
|
|
50
|
+
names: [
|
|
51
|
+
"conversations",
|
|
52
|
+
"messages",
|
|
53
|
+
"tickets",
|
|
54
|
+
"threads",
|
|
55
|
+
"chats",
|
|
56
|
+
"inbox"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
concept: "activity",
|
|
61
|
+
names: [
|
|
62
|
+
"events",
|
|
63
|
+
"activities",
|
|
64
|
+
"jobs",
|
|
65
|
+
"logs",
|
|
66
|
+
"audit",
|
|
67
|
+
"shifts",
|
|
68
|
+
"scans",
|
|
69
|
+
"sessions"
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
concept: "note",
|
|
74
|
+
names: [
|
|
75
|
+
"notes",
|
|
76
|
+
"comments",
|
|
77
|
+
"annotations"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
const CONCEPT_FIELDS = {
|
|
82
|
+
contact: [{
|
|
83
|
+
field: "name",
|
|
84
|
+
candidates: [
|
|
85
|
+
"name",
|
|
86
|
+
"full_name",
|
|
87
|
+
"fullname",
|
|
88
|
+
"display_name"
|
|
89
|
+
],
|
|
90
|
+
required: false
|
|
91
|
+
}, {
|
|
92
|
+
field: "email",
|
|
93
|
+
candidates: ["email", "email_address"],
|
|
94
|
+
required: true
|
|
95
|
+
}],
|
|
96
|
+
company: [{
|
|
97
|
+
field: "name",
|
|
98
|
+
candidates: [
|
|
99
|
+
"name",
|
|
100
|
+
"company_name",
|
|
101
|
+
"legal_name",
|
|
102
|
+
"title"
|
|
103
|
+
],
|
|
104
|
+
required: true
|
|
105
|
+
}, {
|
|
106
|
+
field: "domain",
|
|
107
|
+
candidates: [
|
|
108
|
+
"domain",
|
|
109
|
+
"website",
|
|
110
|
+
"url"
|
|
111
|
+
],
|
|
112
|
+
required: false
|
|
113
|
+
}],
|
|
114
|
+
deal: [
|
|
115
|
+
{
|
|
116
|
+
field: "name",
|
|
117
|
+
candidates: ["name", "title"],
|
|
118
|
+
required: false
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
field: "value",
|
|
122
|
+
candidates: [
|
|
123
|
+
"value",
|
|
124
|
+
"amount",
|
|
125
|
+
"total",
|
|
126
|
+
"price"
|
|
127
|
+
],
|
|
128
|
+
required: false
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
field: "stage",
|
|
132
|
+
candidates: [
|
|
133
|
+
"stage",
|
|
134
|
+
"status",
|
|
135
|
+
"state"
|
|
136
|
+
],
|
|
137
|
+
required: false
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
activity: [
|
|
141
|
+
{
|
|
142
|
+
field: "type",
|
|
143
|
+
candidates: [
|
|
144
|
+
"type",
|
|
145
|
+
"kind",
|
|
146
|
+
"event_type"
|
|
147
|
+
],
|
|
148
|
+
required: false
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
field: "summary",
|
|
152
|
+
candidates: [
|
|
153
|
+
"summary",
|
|
154
|
+
"description",
|
|
155
|
+
"message",
|
|
156
|
+
"title"
|
|
157
|
+
],
|
|
158
|
+
required: false
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
field: "createdAt",
|
|
162
|
+
candidates: [
|
|
163
|
+
"created_at",
|
|
164
|
+
"createdat",
|
|
165
|
+
"timestamp"
|
|
166
|
+
],
|
|
167
|
+
required: false
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
conversation: [{
|
|
171
|
+
field: "status",
|
|
172
|
+
candidates: ["status", "state"],
|
|
173
|
+
required: false
|
|
174
|
+
}, {
|
|
175
|
+
field: "subject",
|
|
176
|
+
candidates: [
|
|
177
|
+
"subject",
|
|
178
|
+
"title",
|
|
179
|
+
"preview"
|
|
180
|
+
],
|
|
181
|
+
required: false
|
|
182
|
+
}],
|
|
183
|
+
note: [{
|
|
184
|
+
field: "body",
|
|
185
|
+
candidates: [
|
|
186
|
+
"body",
|
|
187
|
+
"content",
|
|
188
|
+
"text",
|
|
189
|
+
"note"
|
|
190
|
+
],
|
|
191
|
+
required: true
|
|
192
|
+
}]
|
|
193
|
+
};
|
|
194
|
+
const CURSOR_CANDIDATES = [
|
|
195
|
+
"updatedat",
|
|
196
|
+
"updated_at",
|
|
197
|
+
"modifiedat",
|
|
198
|
+
"modified_at",
|
|
199
|
+
"createdat",
|
|
200
|
+
"created_at"
|
|
201
|
+
];
|
|
202
|
+
function suggestCursorField(columnKeys) {
|
|
203
|
+
const lowerToActual = new Map(columnKeys.map((c) => [c.toLowerCase(), c]));
|
|
204
|
+
for (const candidate of CURSOR_CANDIDATES) {
|
|
205
|
+
const match = lowerToActual.get(candidate);
|
|
206
|
+
if (match) return match;
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function suggestConcept(tableName) {
|
|
211
|
+
const normalized = tableName.toLowerCase();
|
|
212
|
+
for (const { concept, names } of TABLE_PATTERNS) if (names.includes(normalized)) return concept;
|
|
213
|
+
for (const { concept, names } of TABLE_PATTERNS) if (names.some((name) => normalized.includes(name))) return concept;
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
function matchesConcept(tableName, concept) {
|
|
217
|
+
const normalized = tableName.toLowerCase();
|
|
218
|
+
const entry = TABLE_PATTERNS.find((p) => p.concept === concept);
|
|
219
|
+
if (!entry) return null;
|
|
220
|
+
if (entry.names.includes(normalized)) return { exact: true };
|
|
221
|
+
if (entry.names.some((name) => normalized.includes(name))) return { exact: false };
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
function suggestFieldMap(concept, columns) {
|
|
225
|
+
const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
|
|
226
|
+
const map = {};
|
|
227
|
+
for (const { field, candidates } of CONCEPT_FIELDS[concept]) for (const candidate of candidates) {
|
|
228
|
+
const match = lowerToActual.get(candidate);
|
|
229
|
+
if (match) {
|
|
230
|
+
map[field] = match;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
236
|
+
function requiredFieldsFor(concept) {
|
|
237
|
+
return CONCEPT_FIELDS[concept].filter((f) => f.required).map((f) => f.field);
|
|
238
|
+
}
|
|
239
|
+
function validateMapping(requiredFields, fieldMap) {
|
|
240
|
+
const missing = requiredFields.filter((field) => !fieldMap[field]);
|
|
241
|
+
return {
|
|
242
|
+
valid: missing.length === 0,
|
|
243
|
+
missing
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const DISCRIMINATOR_COLUMNS = [
|
|
247
|
+
"role",
|
|
248
|
+
"type",
|
|
249
|
+
"kind",
|
|
250
|
+
"status",
|
|
251
|
+
"state",
|
|
252
|
+
"category",
|
|
253
|
+
"tier"
|
|
254
|
+
];
|
|
255
|
+
function suggestDiscriminatorColumns(columns) {
|
|
256
|
+
const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
|
|
257
|
+
const out = [];
|
|
258
|
+
for (const name of DISCRIMINATOR_COLUMNS) {
|
|
259
|
+
const match = lowerToActual.get(name);
|
|
260
|
+
if (match) out.push(match);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
const CONTACT_FK_HINTS = [
|
|
265
|
+
"user_id",
|
|
266
|
+
"owner_id",
|
|
267
|
+
"contact_id",
|
|
268
|
+
"member_id",
|
|
269
|
+
"person_id",
|
|
270
|
+
"created_by"
|
|
271
|
+
];
|
|
272
|
+
const COMPANY_FK_HINTS = [
|
|
273
|
+
"organization_id",
|
|
274
|
+
"organisation_id",
|
|
275
|
+
"org_id",
|
|
276
|
+
"company_id",
|
|
277
|
+
"account_id",
|
|
278
|
+
"team_id"
|
|
279
|
+
];
|
|
280
|
+
function firstMatchingColumn(columns, hints) {
|
|
281
|
+
const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
|
|
282
|
+
for (const hint of hints) {
|
|
283
|
+
const match = lowerToActual.get(hint);
|
|
284
|
+
if (match) return match;
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Suggest contact→company relationships for a contact-concept source table by
|
|
290
|
+
* inspecting FK column names across the discovered schema. Detects all three
|
|
291
|
+
* DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
|
|
292
|
+
* candidates only — the developer confirms.
|
|
293
|
+
*/
|
|
294
|
+
function suggestRelationships(source, allTables) {
|
|
295
|
+
const companyTables = allTables.filter((t) => t.name !== source.name && suggestConcept(t.name) === "company");
|
|
296
|
+
if (companyTables.length === 0) return [];
|
|
297
|
+
const suggestions = [];
|
|
298
|
+
const contactFk = firstMatchingColumn(source.columns, COMPANY_FK_HINTS);
|
|
299
|
+
if (contactFk) suggestions.push({
|
|
300
|
+
name: "company",
|
|
301
|
+
entityTable: companyTables[0]?.name ?? "",
|
|
302
|
+
via: {
|
|
303
|
+
fkOn: "contact",
|
|
304
|
+
column: `${source.name}.${contactFk}`
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
for (const company of companyTables) {
|
|
308
|
+
const companyFk = firstMatchingColumn(company.columns, CONTACT_FK_HINTS);
|
|
309
|
+
if (companyFk) suggestions.push({
|
|
310
|
+
name: "company",
|
|
311
|
+
entityTable: company.name,
|
|
312
|
+
via: {
|
|
313
|
+
fkOn: "company",
|
|
314
|
+
column: `${company.name}.${companyFk}`
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
for (const table of allTables) {
|
|
319
|
+
if (table.name === source.name) continue;
|
|
320
|
+
const localKey = firstMatchingColumn(table.columns, CONTACT_FK_HINTS);
|
|
321
|
+
const foreignKey = firstMatchingColumn(table.columns, COMPANY_FK_HINTS);
|
|
322
|
+
if (localKey && foreignKey) suggestions.push({
|
|
323
|
+
name: "company",
|
|
324
|
+
entityTable: companyTables.find((c) => c.name !== table.name)?.name ?? companyTables[0]?.name ?? "",
|
|
325
|
+
via: {
|
|
326
|
+
through: table.name,
|
|
327
|
+
localKey,
|
|
328
|
+
foreignKey
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return suggestions;
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
export { TARGET_CONCEPTS, matchesConcept, requiredFieldsFor, suggestConcept, suggestCursorField, suggestDiscriminatorColumns, suggestFieldMap, suggestRelationships, validateMapping };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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.authCustom = require_auth_adapter.authCustom;
|
|
12
|
+
exports.buildFilterFromDiscriminator = require_mapping_form.buildFilterFromDiscriminator;
|
|
13
|
+
exports.buildRelationships = require_mapping_form.buildRelationships;
|
|
14
|
+
exports.compileEntityView = require_view_compiler.compileEntityView;
|
|
15
|
+
exports.compileEntityViews = require_view_compiler.compileEntityViews;
|
|
16
|
+
exports.defineClivlyConfig = require_entity_config.defineClivlyConfig;
|
|
17
|
+
exports.entitiesConfigSchema = require_entity_config.entitiesConfigSchema;
|
|
18
|
+
exports.explainEntitiesConfig = require_view_compiler.explainEntitiesConfig;
|
|
19
|
+
exports.filterSchema = require_entity_config.filterSchema;
|
|
20
|
+
exports.mappingsToEntitiesConfig = require_mappings_config.mappingsToEntitiesConfig;
|
|
21
|
+
exports.reconcile = require_sync_engine.reconcile;
|
|
22
|
+
exports.relationshipSchema = require_entity_config.relationshipSchema;
|
|
23
|
+
exports.relationshipViaSignature = require_mapping_form.relationshipViaSignature;
|
|
24
|
+
exports.relationshipsSchema = require_entity_config.relationshipsSchema;
|
|
25
|
+
exports.runSync = require_sync_engine.runSync;
|
|
26
|
+
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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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,99 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_entity_config = require("./entity-config.cjs");
|
|
3
|
+
//#region src/mappings-config.ts
|
|
4
|
+
const RESERVED_FIELD = "id";
|
|
5
|
+
/**
|
|
6
|
+
* Restrict a persisted field map to what the mirror can actually store: drop
|
|
7
|
+
* the reserved `id` key (for every concept) and, for materialized concepts
|
|
8
|
+
* (contact/company), any key that isn't canonical. This makes the pipeline
|
|
9
|
+
* resilient to legacy or mis-suggested field maps — a stray `id` no longer
|
|
10
|
+
* blocks the whole sync.
|
|
11
|
+
*/
|
|
12
|
+
function canonicalFields(concept, fieldMap) {
|
|
13
|
+
const allowed = require_entity_config.CANONICAL_FIELDS[concept];
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [field, column] of Object.entries(fieldMap)) {
|
|
16
|
+
if (field === RESERVED_FIELD) continue;
|
|
17
|
+
if (allowed && !allowed.includes(field)) continue;
|
|
18
|
+
result[field] = column;
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
const DEFAULT_ROLE = "primary";
|
|
23
|
+
const ACTIVE_STATUS = "active";
|
|
24
|
+
function isEligible(row) {
|
|
25
|
+
return (row.status ?? ACTIVE_STATUS) === ACTIVE_STATUS && Object.keys(row.fieldMap ?? {}).length > 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Preferred key for a row: the bare source table when it backs a single entity,
|
|
29
|
+
* else role-suffixed so multiple roles on one table stay distinct and stable
|
|
30
|
+
* (the primary role keeps the bare name). Uniqueness is still enforced by the
|
|
31
|
+
* caller in case two rows land on the same preferred key.
|
|
32
|
+
*/
|
|
33
|
+
function preferredKey(row, tableCounts) {
|
|
34
|
+
const role = row.role ?? DEFAULT_ROLE;
|
|
35
|
+
if ((tableCounts.get(row.sourceTable) ?? 0) <= 1) return row.sourceTable;
|
|
36
|
+
return role === DEFAULT_ROLE ? row.sourceTable : `${row.sourceTable}__${role}`;
|
|
37
|
+
}
|
|
38
|
+
function uniquify(base, used) {
|
|
39
|
+
if (!used.has(base)) {
|
|
40
|
+
used.add(base);
|
|
41
|
+
return base;
|
|
42
|
+
}
|
|
43
|
+
let suffix = 2;
|
|
44
|
+
while (used.has(`${base}__${suffix}`)) suffix++;
|
|
45
|
+
const key = `${base}__${suffix}`;
|
|
46
|
+
used.add(key);
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
function buildEntity(row, refIndex) {
|
|
50
|
+
const entity = {
|
|
51
|
+
concept: row.targetConcept,
|
|
52
|
+
source: row.sourceTable,
|
|
53
|
+
role: row.role ?? DEFAULT_ROLE,
|
|
54
|
+
fields: canonicalFields(row.targetConcept, row.fieldMap)
|
|
55
|
+
};
|
|
56
|
+
if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
|
|
57
|
+
if (row.relationships && Object.keys(row.relationships).length > 0) {
|
|
58
|
+
const relationships = {};
|
|
59
|
+
for (const [name, spec] of Object.entries(row.relationships)) {
|
|
60
|
+
const rel = spec;
|
|
61
|
+
relationships[name] = {
|
|
62
|
+
entity: refIndex.get(rel.entity) ?? rel.entity,
|
|
63
|
+
via: rel.via
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
entity.relationships = relationships;
|
|
67
|
+
}
|
|
68
|
+
return entity;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Compile persisted mapping rows into a `ClivlyEntitiesConfig`. Only `active`
|
|
72
|
+
* rows with at least one mapped field are included; disabled/suggested rows and
|
|
73
|
+
* unmapped rows are skipped. Returns `{ entities: {} }` when nothing is
|
|
74
|
+
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
75
|
+
*/
|
|
76
|
+
function mappingsToEntitiesConfig(rows) {
|
|
77
|
+
const eligible = rows.filter(isEligible);
|
|
78
|
+
const tableCounts = /* @__PURE__ */ new Map();
|
|
79
|
+
for (const row of eligible) tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
|
|
80
|
+
const used = /* @__PURE__ */ new Set();
|
|
81
|
+
const tableAnchor = /* @__PURE__ */ new Map();
|
|
82
|
+
const assigned = [];
|
|
83
|
+
for (const row of eligible) {
|
|
84
|
+
const key = uniquify(preferredKey(row, tableCounts), used);
|
|
85
|
+
assigned.push({
|
|
86
|
+
row,
|
|
87
|
+
key
|
|
88
|
+
});
|
|
89
|
+
if ((row.role ?? DEFAULT_ROLE) === DEFAULT_ROLE || !tableAnchor.has(row.sourceTable)) tableAnchor.set(row.sourceTable, key);
|
|
90
|
+
}
|
|
91
|
+
const refIndex = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const { key } of assigned) refIndex.set(key, key);
|
|
93
|
+
for (const [table, key] of tableAnchor) if (!refIndex.has(table)) refIndex.set(table, key);
|
|
94
|
+
const entities = {};
|
|
95
|
+
for (const { row, key } of assigned) entities[key] = buildEntity(row, refIndex);
|
|
96
|
+
return { entities };
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
exports.mappingsToEntitiesConfig = mappingsToEntitiesConfig;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ClivlyEntitiesConfig } from "./entity-config.cjs";
|
|
2
|
+
|
|
3
|
+
//#region src/mappings-config.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Mappings → Entity Config bridge.
|
|
6
|
+
*
|
|
7
|
+
* `crm_entity_mappings` rows are the single source of truth for how a host
|
|
8
|
+
* schema maps onto Clivly's CRM concepts. This module compiles those rows into
|
|
9
|
+
* a `ClivlyEntitiesConfig` — the contract the view compiler and sync engine
|
|
10
|
+
* consume — so the persisted mapping and the runtime pipeline never diverge.
|
|
11
|
+
*
|
|
12
|
+
* The row model has no explicit entity-key column (it's keyed by
|
|
13
|
+
* `sourceTable + targetConcept + role`), but the config's `relationships`
|
|
14
|
+
* reference other entities *by key*. This builder therefore derives a stable
|
|
15
|
+
* key per row and resolves each relationship's `entity` reference through the
|
|
16
|
+
* same derivation — so a relationship stored as `entity: "<relatedTable>"`
|
|
17
|
+
* (the shape `mappings.suggest` produces) resolves to the right entity.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* A persisted `crm_entity_mappings` row, narrowed to the columns the builder
|
|
21
|
+
* reads. Structurally compatible with the Drizzle row (jsonb columns typed
|
|
22
|
+
* loosely) so the API layer can pass `db.select()` results straight in.
|
|
23
|
+
*/
|
|
24
|
+
interface EntityMappingRow {
|
|
25
|
+
fieldMap: Record<string, string>;
|
|
26
|
+
filter?: Record<string, unknown> | null;
|
|
27
|
+
relationships?: Record<string, unknown> | null;
|
|
28
|
+
role?: string | null;
|
|
29
|
+
sourceTable: string;
|
|
30
|
+
status?: string | null;
|
|
31
|
+
targetConcept: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compile persisted mapping rows into a `ClivlyEntitiesConfig`. Only `active`
|
|
35
|
+
* rows with at least one mapped field are included; disabled/suggested rows and
|
|
36
|
+
* unmapped rows are skipped. Returns `{ entities: {} }` when nothing is
|
|
37
|
+
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
38
|
+
*/
|
|
39
|
+
declare function mappingsToEntitiesConfig(rows: EntityMappingRow[]): ClivlyEntitiesConfig;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { EntityMappingRow, mappingsToEntitiesConfig };
|
package/dist/mappings-config.mjs
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
+
import { CANONICAL_FIELDS } from "./entity-config.mjs";
|
|
1
2
|
//#region src/mappings-config.ts
|
|
3
|
+
const RESERVED_FIELD = "id";
|
|
4
|
+
/**
|
|
5
|
+
* Restrict a persisted field map to what the mirror can actually store: drop
|
|
6
|
+
* the reserved `id` key (for every concept) and, for materialized concepts
|
|
7
|
+
* (contact/company), any key that isn't canonical. This makes the pipeline
|
|
8
|
+
* resilient to legacy or mis-suggested field maps — a stray `id` no longer
|
|
9
|
+
* blocks the whole sync.
|
|
10
|
+
*/
|
|
11
|
+
function canonicalFields(concept, fieldMap) {
|
|
12
|
+
const allowed = CANONICAL_FIELDS[concept];
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [field, column] of Object.entries(fieldMap)) {
|
|
15
|
+
if (field === RESERVED_FIELD) continue;
|
|
16
|
+
if (allowed && !allowed.includes(field)) continue;
|
|
17
|
+
result[field] = column;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
2
21
|
const DEFAULT_ROLE = "primary";
|
|
3
22
|
const ACTIVE_STATUS = "active";
|
|
4
23
|
function isEligible(row) {
|
|
@@ -31,7 +50,7 @@ function buildEntity(row, refIndex) {
|
|
|
31
50
|
concept: row.targetConcept,
|
|
32
51
|
source: row.sourceTable,
|
|
33
52
|
role: row.role ?? DEFAULT_ROLE,
|
|
34
|
-
fields:
|
|
53
|
+
fields: canonicalFields(row.targetConcept, row.fieldMap)
|
|
35
54
|
};
|
|
36
55
|
if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
|
|
37
56
|
if (row.relationships && Object.keys(row.relationships).length > 0) {
|