@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.
@@ -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;
@@ -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 };
@@ -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: { ...row.fieldMap }
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) {