@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,344 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/entity-heuristics.ts
3
+ const TARGET_CONCEPTS = [
4
+ "contact",
5
+ "company",
6
+ "deal",
7
+ "activity",
8
+ "conversation",
9
+ "note"
10
+ ];
11
+ const TABLE_PATTERNS = [
12
+ {
13
+ concept: "contact",
14
+ names: [
15
+ "users",
16
+ "customers",
17
+ "clients",
18
+ "accounts",
19
+ "participants",
20
+ "attendees",
21
+ "people",
22
+ "members",
23
+ "contacts"
24
+ ]
25
+ },
26
+ {
27
+ concept: "company",
28
+ names: [
29
+ "companies",
30
+ "organizations",
31
+ "organisations",
32
+ "orgs",
33
+ "teams",
34
+ "businesses",
35
+ "accounts"
36
+ ]
37
+ },
38
+ {
39
+ concept: "deal",
40
+ names: [
41
+ "deals",
42
+ "opportunities",
43
+ "subscriptions",
44
+ "orders",
45
+ "invoices",
46
+ "sales"
47
+ ]
48
+ },
49
+ {
50
+ concept: "conversation",
51
+ names: [
52
+ "conversations",
53
+ "messages",
54
+ "tickets",
55
+ "threads",
56
+ "chats",
57
+ "inbox"
58
+ ]
59
+ },
60
+ {
61
+ concept: "activity",
62
+ names: [
63
+ "events",
64
+ "activities",
65
+ "jobs",
66
+ "logs",
67
+ "audit",
68
+ "shifts",
69
+ "scans",
70
+ "sessions"
71
+ ]
72
+ },
73
+ {
74
+ concept: "note",
75
+ names: [
76
+ "notes",
77
+ "comments",
78
+ "annotations"
79
+ ]
80
+ }
81
+ ];
82
+ const CONCEPT_FIELDS = {
83
+ contact: [{
84
+ field: "name",
85
+ candidates: [
86
+ "name",
87
+ "full_name",
88
+ "fullname",
89
+ "display_name"
90
+ ],
91
+ required: false
92
+ }, {
93
+ field: "email",
94
+ candidates: ["email", "email_address"],
95
+ required: true
96
+ }],
97
+ company: [{
98
+ field: "name",
99
+ candidates: [
100
+ "name",
101
+ "company_name",
102
+ "legal_name",
103
+ "title"
104
+ ],
105
+ required: true
106
+ }, {
107
+ field: "domain",
108
+ candidates: [
109
+ "domain",
110
+ "website",
111
+ "url"
112
+ ],
113
+ required: false
114
+ }],
115
+ deal: [
116
+ {
117
+ field: "name",
118
+ candidates: ["name", "title"],
119
+ required: false
120
+ },
121
+ {
122
+ field: "value",
123
+ candidates: [
124
+ "value",
125
+ "amount",
126
+ "total",
127
+ "price"
128
+ ],
129
+ required: false
130
+ },
131
+ {
132
+ field: "stage",
133
+ candidates: [
134
+ "stage",
135
+ "status",
136
+ "state"
137
+ ],
138
+ required: false
139
+ }
140
+ ],
141
+ activity: [
142
+ {
143
+ field: "type",
144
+ candidates: [
145
+ "type",
146
+ "kind",
147
+ "event_type"
148
+ ],
149
+ required: false
150
+ },
151
+ {
152
+ field: "summary",
153
+ candidates: [
154
+ "summary",
155
+ "description",
156
+ "message",
157
+ "title"
158
+ ],
159
+ required: false
160
+ },
161
+ {
162
+ field: "createdAt",
163
+ candidates: [
164
+ "created_at",
165
+ "createdat",
166
+ "timestamp"
167
+ ],
168
+ required: false
169
+ }
170
+ ],
171
+ conversation: [{
172
+ field: "status",
173
+ candidates: ["status", "state"],
174
+ required: false
175
+ }, {
176
+ field: "subject",
177
+ candidates: [
178
+ "subject",
179
+ "title",
180
+ "preview"
181
+ ],
182
+ required: false
183
+ }],
184
+ note: [{
185
+ field: "body",
186
+ candidates: [
187
+ "body",
188
+ "content",
189
+ "text",
190
+ "note"
191
+ ],
192
+ required: true
193
+ }]
194
+ };
195
+ const CURSOR_CANDIDATES = [
196
+ "updatedat",
197
+ "updated_at",
198
+ "modifiedat",
199
+ "modified_at",
200
+ "createdat",
201
+ "created_at"
202
+ ];
203
+ function suggestCursorField(columnKeys) {
204
+ const lowerToActual = new Map(columnKeys.map((c) => [c.toLowerCase(), c]));
205
+ for (const candidate of CURSOR_CANDIDATES) {
206
+ const match = lowerToActual.get(candidate);
207
+ if (match) return match;
208
+ }
209
+ return null;
210
+ }
211
+ function suggestConcept(tableName) {
212
+ const normalized = tableName.toLowerCase();
213
+ for (const { concept, names } of TABLE_PATTERNS) if (names.includes(normalized)) return concept;
214
+ for (const { concept, names } of TABLE_PATTERNS) if (names.some((name) => normalized.includes(name))) return concept;
215
+ return null;
216
+ }
217
+ function matchesConcept(tableName, concept) {
218
+ const normalized = tableName.toLowerCase();
219
+ const entry = TABLE_PATTERNS.find((p) => p.concept === concept);
220
+ if (!entry) return null;
221
+ if (entry.names.includes(normalized)) return { exact: true };
222
+ if (entry.names.some((name) => normalized.includes(name))) return { exact: false };
223
+ return null;
224
+ }
225
+ function suggestFieldMap(concept, columns) {
226
+ const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
227
+ const map = {};
228
+ for (const { field, candidates } of CONCEPT_FIELDS[concept]) for (const candidate of candidates) {
229
+ const match = lowerToActual.get(candidate);
230
+ if (match) {
231
+ map[field] = match;
232
+ break;
233
+ }
234
+ }
235
+ return map;
236
+ }
237
+ function requiredFieldsFor(concept) {
238
+ return CONCEPT_FIELDS[concept].filter((f) => f.required).map((f) => f.field);
239
+ }
240
+ function validateMapping(requiredFields, fieldMap) {
241
+ const missing = requiredFields.filter((field) => !fieldMap[field]);
242
+ return {
243
+ valid: missing.length === 0,
244
+ missing
245
+ };
246
+ }
247
+ const DISCRIMINATOR_COLUMNS = [
248
+ "role",
249
+ "type",
250
+ "kind",
251
+ "status",
252
+ "state",
253
+ "category",
254
+ "tier"
255
+ ];
256
+ function suggestDiscriminatorColumns(columns) {
257
+ const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
258
+ const out = [];
259
+ for (const name of DISCRIMINATOR_COLUMNS) {
260
+ const match = lowerToActual.get(name);
261
+ if (match) out.push(match);
262
+ }
263
+ return out;
264
+ }
265
+ const CONTACT_FK_HINTS = [
266
+ "user_id",
267
+ "owner_id",
268
+ "contact_id",
269
+ "member_id",
270
+ "person_id",
271
+ "created_by"
272
+ ];
273
+ const COMPANY_FK_HINTS = [
274
+ "organization_id",
275
+ "organisation_id",
276
+ "org_id",
277
+ "company_id",
278
+ "account_id",
279
+ "team_id"
280
+ ];
281
+ function firstMatchingColumn(columns, hints) {
282
+ const lowerToActual = new Map(columns.map((c) => [c.toLowerCase(), c]));
283
+ for (const hint of hints) {
284
+ const match = lowerToActual.get(hint);
285
+ if (match) return match;
286
+ }
287
+ return null;
288
+ }
289
+ /**
290
+ * Suggest contact→company relationships for a contact-concept source table by
291
+ * inspecting FK column names across the discovered schema. Detects all three
292
+ * DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
293
+ * candidates only — the developer confirms.
294
+ */
295
+ function suggestRelationships(source, allTables) {
296
+ const companyTables = allTables.filter((t) => t.name !== source.name && suggestConcept(t.name) === "company");
297
+ if (companyTables.length === 0) return [];
298
+ const suggestions = [];
299
+ const contactFk = firstMatchingColumn(source.columns, COMPANY_FK_HINTS);
300
+ if (contactFk) suggestions.push({
301
+ name: "company",
302
+ entityTable: companyTables[0]?.name ?? "",
303
+ via: {
304
+ fkOn: "contact",
305
+ column: `${source.name}.${contactFk}`
306
+ }
307
+ });
308
+ for (const company of companyTables) {
309
+ const companyFk = firstMatchingColumn(company.columns, CONTACT_FK_HINTS);
310
+ if (companyFk) suggestions.push({
311
+ name: "company",
312
+ entityTable: company.name,
313
+ via: {
314
+ fkOn: "company",
315
+ column: `${company.name}.${companyFk}`
316
+ }
317
+ });
318
+ }
319
+ for (const table of allTables) {
320
+ if (table.name === source.name) continue;
321
+ const localKey = firstMatchingColumn(table.columns, CONTACT_FK_HINTS);
322
+ const foreignKey = firstMatchingColumn(table.columns, COMPANY_FK_HINTS);
323
+ if (localKey && foreignKey) suggestions.push({
324
+ name: "company",
325
+ entityTable: companyTables.find((c) => c.name !== table.name)?.name ?? companyTables[0]?.name ?? "",
326
+ via: {
327
+ through: table.name,
328
+ localKey,
329
+ foreignKey
330
+ }
331
+ });
332
+ }
333
+ return suggestions;
334
+ }
335
+ //#endregion
336
+ exports.TARGET_CONCEPTS = TARGET_CONCEPTS;
337
+ exports.matchesConcept = matchesConcept;
338
+ exports.requiredFieldsFor = requiredFieldsFor;
339
+ exports.suggestConcept = suggestConcept;
340
+ exports.suggestCursorField = suggestCursorField;
341
+ exports.suggestDiscriminatorColumns = suggestDiscriminatorColumns;
342
+ exports.suggestFieldMap = suggestFieldMap;
343
+ exports.suggestRelationships = suggestRelationships;
344
+ exports.validateMapping = validateMapping;
@@ -0,0 +1,43 @@
1
+ //#region src/entity-heuristics.d.ts
2
+ declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
3
+ type TargetConcept = (typeof TARGET_CONCEPTS)[number];
4
+ declare function suggestCursorField(columnKeys: string[]): string | null;
5
+ declare function suggestConcept(tableName: string): TargetConcept | null;
6
+ declare function matchesConcept(tableName: string, concept: TargetConcept): {
7
+ exact: boolean;
8
+ } | null;
9
+ declare function suggestFieldMap(concept: TargetConcept, columns: string[]): Record<string, string>;
10
+ declare function requiredFieldsFor(concept: TargetConcept): string[];
11
+ declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
12
+ valid: boolean;
13
+ missing: string[];
14
+ };
15
+ declare function suggestDiscriminatorColumns(columns: string[]): string[];
16
+ type SuggestedVia = {
17
+ fkOn: "company" | "contact";
18
+ column: string;
19
+ } | {
20
+ through: string;
21
+ localKey: string;
22
+ foreignKey: string;
23
+ };
24
+ interface RelationshipSuggestion {
25
+ /** The related host table. */
26
+ entityTable: string;
27
+ /** Suggested relationship name (defaults to the related concept). */
28
+ name: string;
29
+ via: SuggestedVia;
30
+ }
31
+ interface HeuristicTable {
32
+ columns: string[];
33
+ name: string;
34
+ }
35
+ /**
36
+ * Suggest contact→company relationships for a contact-concept source table by
37
+ * inspecting FK column names across the discovered schema. Detects all three
38
+ * DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
39
+ * candidates only — the developer confirms.
40
+ */
41
+ declare function suggestRelationships(source: HeuristicTable, allTables: HeuristicTable[]): RelationshipSuggestion[];
42
+ //#endregion
43
+ export { RelationshipSuggestion, SuggestedVia, TARGET_CONCEPTS, TargetConcept, matchesConcept, requiredFieldsFor, suggestConcept, suggestCursorField, suggestDiscriminatorColumns, suggestFieldMap, suggestRelationships, validateMapping };
@@ -0,0 +1,43 @@
1
+ //#region src/entity-heuristics.d.ts
2
+ declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
3
+ type TargetConcept = (typeof TARGET_CONCEPTS)[number];
4
+ declare function suggestCursorField(columnKeys: string[]): string | null;
5
+ declare function suggestConcept(tableName: string): TargetConcept | null;
6
+ declare function matchesConcept(tableName: string, concept: TargetConcept): {
7
+ exact: boolean;
8
+ } | null;
9
+ declare function suggestFieldMap(concept: TargetConcept, columns: string[]): Record<string, string>;
10
+ declare function requiredFieldsFor(concept: TargetConcept): string[];
11
+ declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
12
+ valid: boolean;
13
+ missing: string[];
14
+ };
15
+ declare function suggestDiscriminatorColumns(columns: string[]): string[];
16
+ type SuggestedVia = {
17
+ fkOn: "company" | "contact";
18
+ column: string;
19
+ } | {
20
+ through: string;
21
+ localKey: string;
22
+ foreignKey: string;
23
+ };
24
+ interface RelationshipSuggestion {
25
+ /** The related host table. */
26
+ entityTable: string;
27
+ /** Suggested relationship name (defaults to the related concept). */
28
+ name: string;
29
+ via: SuggestedVia;
30
+ }
31
+ interface HeuristicTable {
32
+ columns: string[];
33
+ name: string;
34
+ }
35
+ /**
36
+ * Suggest contact→company relationships for a contact-concept source table by
37
+ * inspecting FK column names across the discovered schema. Detects all three
38
+ * DSL shapes: FK-on-contact, FK-on-company, and a join table. Suggestions are
39
+ * candidates only — the developer confirms.
40
+ */
41
+ declare function suggestRelationships(source: HeuristicTable, allTables: HeuristicTable[]): RelationshipSuggestion[];
42
+ //#endregion
43
+ export { RelationshipSuggestion, SuggestedVia, TARGET_CONCEPTS, TargetConcept, matchesConcept, requiredFieldsFor, suggestConcept, suggestCursorField, suggestDiscriminatorColumns, suggestFieldMap, suggestRelationships, validateMapping };