@clivly/core 0.2.0 → 0.3.0-next.2
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/dist/adapter.d.cts +13 -3
- package/dist/adapter.d.mts +13 -3
- package/dist/entity-config.cjs +39 -3
- package/dist/entity-config.d.cts +29 -5
- package/dist/entity-config.d.mts +29 -5
- package/dist/entity-config.mjs +38 -4
- package/dist/entity-heuristics.cjs +160 -62
- package/dist/entity-heuristics.d.cts +38 -13
- package/dist/entity-heuristics.d.mts +38 -13
- package/dist/entity-heuristics.mjs +156 -60
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- 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 +1 -0
- package/dist/mappings-config.d.cts +1 -0
- package/dist/mappings-config.d.mts +1 -0
- package/dist/mappings-config.mjs +1 -0
- package/dist/sync-engine.cjs +54 -4
- package/dist/sync-engine.d.cts +40 -5
- package/dist/sync-engine.d.mts +40 -5
- package/dist/sync-engine.mjs +53 -5
- package/dist/types.d.cts +4 -1
- package/dist/types.d.mts +4 -1
- package/dist/view-compiler.d.cts +7 -0
- package/dist/view-compiler.d.mts +7 -0
- package/package.json +12 -2
package/dist/adapter.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Activity, Company, Contact, ContactFilters, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, Deal, Task, TaskFilters, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.cjs";
|
|
1
|
+
import { Activity, Company, Contact, ContactFilters, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, Deal, RecordSource, Task, TaskFilters, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/adapter.d.ts
|
|
4
4
|
/**
|
|
@@ -19,12 +19,22 @@ interface CRMAdapter {
|
|
|
19
19
|
createTask(orgId: string, data: CreateTaskInput): Promise<Task>;
|
|
20
20
|
deleteDeal(orgId: string, id: string): Promise<Deal | null>;
|
|
21
21
|
getActivities(orgId: string, contactId: string): Promise<Activity[]>;
|
|
22
|
-
getCompanies(orgId: string
|
|
22
|
+
getCompanies(orgId: string, filters?: {
|
|
23
|
+
source?: RecordSource;
|
|
24
|
+
}): Promise<Company[]>;
|
|
23
25
|
getContact(orgId: string, id: string): Promise<Contact | null>;
|
|
24
26
|
getContacts(orgId: string, filters?: ContactFilters): Promise<Contact[]>;
|
|
25
27
|
getDeal(orgId: string, id: string): Promise<Deal | null>;
|
|
26
|
-
getDeals(orgId: string
|
|
28
|
+
getDeals(orgId: string, filters?: {
|
|
29
|
+
source?: RecordSource;
|
|
30
|
+
}): Promise<Deal[]>;
|
|
27
31
|
getTasks(orgId: string, filters?: TaskFilters): Promise<Task[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Move a deal to `stage` and reorder it directly after `afterId` (null = top
|
|
34
|
+
* of the column). The adapter computes the fractional `position` from the
|
|
35
|
+
* target column's current order; callers never pass a numeric position.
|
|
36
|
+
*/
|
|
37
|
+
moveDeal(orgId: string, id: string, stage: Deal["stage"], afterId: string | null): Promise<Deal>;
|
|
28
38
|
reopenTask(orgId: string, id: string): Promise<Task>;
|
|
29
39
|
updateContact(orgId: string, id: string, data: UpdateContactInput): Promise<Contact>;
|
|
30
40
|
updateDeal(orgId: string, id: string, data: UpdateDealInput): Promise<Deal>;
|
package/dist/adapter.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Activity, Company, Contact, ContactFilters, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, Deal, Task, TaskFilters, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.mjs";
|
|
1
|
+
import { Activity, Company, Contact, ContactFilters, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, Deal, RecordSource, Task, TaskFilters, UpdateContactInput, UpdateDealInput, UpdateTaskInput } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/adapter.d.ts
|
|
4
4
|
/**
|
|
@@ -19,12 +19,22 @@ interface CRMAdapter {
|
|
|
19
19
|
createTask(orgId: string, data: CreateTaskInput): Promise<Task>;
|
|
20
20
|
deleteDeal(orgId: string, id: string): Promise<Deal | null>;
|
|
21
21
|
getActivities(orgId: string, contactId: string): Promise<Activity[]>;
|
|
22
|
-
getCompanies(orgId: string
|
|
22
|
+
getCompanies(orgId: string, filters?: {
|
|
23
|
+
source?: RecordSource;
|
|
24
|
+
}): Promise<Company[]>;
|
|
23
25
|
getContact(orgId: string, id: string): Promise<Contact | null>;
|
|
24
26
|
getContacts(orgId: string, filters?: ContactFilters): Promise<Contact[]>;
|
|
25
27
|
getDeal(orgId: string, id: string): Promise<Deal | null>;
|
|
26
|
-
getDeals(orgId: string
|
|
28
|
+
getDeals(orgId: string, filters?: {
|
|
29
|
+
source?: RecordSource;
|
|
30
|
+
}): Promise<Deal[]>;
|
|
27
31
|
getTasks(orgId: string, filters?: TaskFilters): Promise<Task[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Move a deal to `stage` and reorder it directly after `afterId` (null = top
|
|
34
|
+
* of the column). The adapter computes the fractional `position` from the
|
|
35
|
+
* target column's current order; callers never pass a numeric position.
|
|
36
|
+
*/
|
|
37
|
+
moveDeal(orgId: string, id: string, stage: Deal["stage"], afterId: string | null): Promise<Deal>;
|
|
28
38
|
reopenTask(orgId: string, id: string): Promise<Task>;
|
|
29
39
|
updateContact(orgId: string, id: string, data: UpdateContactInput): Promise<Contact>;
|
|
30
40
|
updateDeal(orgId: string, id: string, data: UpdateDealInput): Promise<Deal>;
|
package/dist/entity-config.cjs
CHANGED
|
@@ -49,9 +49,38 @@ const CANONICAL_FIELDS = {
|
|
|
49
49
|
"domain",
|
|
50
50
|
"industry",
|
|
51
51
|
"size"
|
|
52
|
+
],
|
|
53
|
+
deal: [
|
|
54
|
+
"name",
|
|
55
|
+
"value",
|
|
56
|
+
"stage"
|
|
52
57
|
]
|
|
53
58
|
};
|
|
54
59
|
/**
|
|
60
|
+
* The concepts the mapping→sync pipeline can actually materialize into `crm_*`
|
|
61
|
+
* tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
|
|
62
|
+
* `persist()` throws for any other concept, so offering one (in the mapping
|
|
63
|
+
* editor, in suggestions, or via the upsert API) is a broken affordance.
|
|
64
|
+
*
|
|
65
|
+
* The broader CRM_CONCEPTS set still validates structurally, so previously
|
|
66
|
+
* stored rows for not-yet-materialized concepts keep rendering. When a concept
|
|
67
|
+
* gains a sync-store path, add its canonical fields above and it joins this set
|
|
68
|
+
* automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
|
|
69
|
+
*/
|
|
70
|
+
const MATERIALIZING_CONCEPTS = [
|
|
71
|
+
"contact",
|
|
72
|
+
"company",
|
|
73
|
+
"deal"
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* The org-defined custom-object target. Deliberately NOT a member of
|
|
77
|
+
* CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
|
|
78
|
+
* no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
|
|
79
|
+
* Handled as an explicit branch wherever concepts are switched on, so the
|
|
80
|
+
* canonical-concept invariants (and their drift test) stay intact.
|
|
81
|
+
*/
|
|
82
|
+
const CUSTOM_CONCEPT = "custom";
|
|
83
|
+
/**
|
|
55
84
|
* Operator forms for a single column predicate. All predicates on an entity are
|
|
56
85
|
* AND-ed together. `$raw` (see FilterExpr) is the escape hatch for anything the
|
|
57
86
|
* operators can't express.
|
|
@@ -125,8 +154,8 @@ const relationshipSchema = zod.z.strictObject({
|
|
|
125
154
|
/** A named map of relationships, as stored on an entity / in the mappings row. */
|
|
126
155
|
const relationshipsSchema = zod.z.record(zod.z.string(), relationshipSchema);
|
|
127
156
|
const entitySchema = zod.z.strictObject({
|
|
128
|
-
/** Which CRM concept this entity maps onto. */
|
|
129
|
-
concept: zod.z.enum(CRM_CONCEPTS),
|
|
157
|
+
/** Which CRM concept this entity maps onto — a canonical concept or "custom". */
|
|
158
|
+
concept: zod.z.union([zod.z.enum(CRM_CONCEPTS), zod.z.literal(CUSTOM_CONCEPT)]),
|
|
130
159
|
/** Host table (or view) this entity is derived from. */
|
|
131
160
|
source: zod.z.string().min(1),
|
|
132
161
|
/**
|
|
@@ -144,7 +173,12 @@ const entitySchema = zod.z.strictObject({
|
|
|
144
173
|
*/
|
|
145
174
|
readOnly: zod.z.array(zod.z.string()).optional(),
|
|
146
175
|
/** Named relationships to other entities in this config. */
|
|
147
|
-
relationships: zod.z.record(zod.z.string(), relationshipSchema).optional()
|
|
176
|
+
relationships: zod.z.record(zod.z.string(), relationshipSchema).optional(),
|
|
177
|
+
/**
|
|
178
|
+
* For a "custom" entity, the crm_object_types row this materializes into.
|
|
179
|
+
* Required for custom (enforced at the API layer), unused for standard concepts.
|
|
180
|
+
*/
|
|
181
|
+
targetObjectTypeId: zod.z.string().min(1).optional()
|
|
148
182
|
});
|
|
149
183
|
const entitiesConfigSchema = zod.z.strictObject({ entities: zod.z.record(zod.z.string(), entitySchema).refine((e) => Object.keys(e).length > 0, { message: "at least one entity must be defined" }) });
|
|
150
184
|
/** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
|
|
@@ -305,7 +339,9 @@ function validateEntitiesConfig(config, schema) {
|
|
|
305
339
|
//#endregion
|
|
306
340
|
exports.CANONICAL_FIELDS = CANONICAL_FIELDS;
|
|
307
341
|
exports.CRM_CONCEPTS = CRM_CONCEPTS;
|
|
342
|
+
exports.CUSTOM_CONCEPT = CUSTOM_CONCEPT;
|
|
308
343
|
exports.ClivlyConfigError = ClivlyConfigError;
|
|
344
|
+
exports.MATERIALIZING_CONCEPTS = MATERIALIZING_CONCEPTS;
|
|
309
345
|
exports.defineClivlyConfig = defineClivlyConfig;
|
|
310
346
|
exports.entitiesConfigSchema = entitiesConfigSchema;
|
|
311
347
|
exports.filterSchema = filterSchema;
|
package/dist/entity-config.d.cts
CHANGED
|
@@ -34,7 +34,29 @@ type CrmConcept = (typeof CRM_CONCEPTS)[number];
|
|
|
34
34
|
declare const CANONICAL_FIELDS: {
|
|
35
35
|
readonly contact: readonly ["name", "email", "title", "status"];
|
|
36
36
|
readonly company: readonly ["name", "domain", "industry", "size"];
|
|
37
|
+
readonly deal: readonly ["name", "value", "stage"];
|
|
37
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The concepts the mapping→sync pipeline can actually materialize into `crm_*`
|
|
41
|
+
* tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
|
|
42
|
+
* `persist()` throws for any other concept, so offering one (in the mapping
|
|
43
|
+
* editor, in suggestions, or via the upsert API) is a broken affordance.
|
|
44
|
+
*
|
|
45
|
+
* The broader CRM_CONCEPTS set still validates structurally, so previously
|
|
46
|
+
* stored rows for not-yet-materialized concepts keep rendering. When a concept
|
|
47
|
+
* gains a sync-store path, add its canonical fields above and it joins this set
|
|
48
|
+
* automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
|
|
49
|
+
*/
|
|
50
|
+
declare const MATERIALIZING_CONCEPTS: readonly ["contact", "company", "deal"];
|
|
51
|
+
type MaterializingConcept = (typeof MATERIALIZING_CONCEPTS)[number];
|
|
52
|
+
/**
|
|
53
|
+
* The org-defined custom-object target. Deliberately NOT a member of
|
|
54
|
+
* CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
|
|
55
|
+
* no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
|
|
56
|
+
* Handled as an explicit branch wherever concepts are switched on, so the
|
|
57
|
+
* canonical-concept invariants (and their drift test) stay intact.
|
|
58
|
+
*/
|
|
59
|
+
declare const CUSTOM_CONCEPT: "custom";
|
|
38
60
|
declare const filterValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
|
|
39
61
|
eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
40
62
|
}, z.core.$strict>, z.ZodObject<{
|
|
@@ -126,14 +148,14 @@ declare const relationshipsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
|
126
148
|
}, z.core.$strict>>;
|
|
127
149
|
type RelationshipsMap = z.infer<typeof relationshipsSchema>;
|
|
128
150
|
declare const entitySchema: z.ZodObject<{
|
|
129
|
-
concept: z.ZodEnum<{
|
|
151
|
+
concept: z.ZodUnion<readonly [z.ZodEnum<{
|
|
130
152
|
contact: "contact";
|
|
131
153
|
company: "company";
|
|
132
154
|
deal: "deal";
|
|
133
155
|
activity: "activity";
|
|
134
156
|
conversation: "conversation";
|
|
135
157
|
note: "note";
|
|
136
|
-
}>;
|
|
158
|
+
}>, z.ZodLiteral<"custom">]>;
|
|
137
159
|
source: z.ZodString;
|
|
138
160
|
role: z.ZodOptional<z.ZodString>;
|
|
139
161
|
filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
@@ -169,18 +191,19 @@ declare const entitySchema: z.ZodObject<{
|
|
|
169
191
|
$raw: z.ZodString;
|
|
170
192
|
}, z.core.$strict>]>;
|
|
171
193
|
}, z.core.$strict>>>;
|
|
194
|
+
targetObjectTypeId: z.ZodOptional<z.ZodString>;
|
|
172
195
|
}, z.core.$strict>;
|
|
173
196
|
type ClivlyEntityConfig = z.infer<typeof entitySchema>;
|
|
174
197
|
declare const entitiesConfigSchema: z.ZodObject<{
|
|
175
198
|
entities: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
176
|
-
concept: z.ZodEnum<{
|
|
199
|
+
concept: z.ZodUnion<readonly [z.ZodEnum<{
|
|
177
200
|
contact: "contact";
|
|
178
201
|
company: "company";
|
|
179
202
|
deal: "deal";
|
|
180
203
|
activity: "activity";
|
|
181
204
|
conversation: "conversation";
|
|
182
205
|
note: "note";
|
|
183
|
-
}>;
|
|
206
|
+
}>, z.ZodLiteral<"custom">]>;
|
|
184
207
|
source: z.ZodString;
|
|
185
208
|
role: z.ZodOptional<z.ZodString>;
|
|
186
209
|
filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
@@ -216,6 +239,7 @@ declare const entitiesConfigSchema: z.ZodObject<{
|
|
|
216
239
|
$raw: z.ZodString;
|
|
217
240
|
}, z.core.$strict>]>;
|
|
218
241
|
}, z.core.$strict>>>;
|
|
242
|
+
targetObjectTypeId: z.ZodOptional<z.ZodString>;
|
|
219
243
|
}, z.core.$strict>>;
|
|
220
244
|
}, z.core.$strict>;
|
|
221
245
|
type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
|
|
@@ -269,4 +293,4 @@ interface ConfigValidationResult {
|
|
|
269
293
|
*/
|
|
270
294
|
declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
|
|
271
295
|
//#endregion
|
|
272
|
-
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
|
296
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
package/dist/entity-config.d.mts
CHANGED
|
@@ -34,7 +34,29 @@ type CrmConcept = (typeof CRM_CONCEPTS)[number];
|
|
|
34
34
|
declare const CANONICAL_FIELDS: {
|
|
35
35
|
readonly contact: readonly ["name", "email", "title", "status"];
|
|
36
36
|
readonly company: readonly ["name", "domain", "industry", "size"];
|
|
37
|
+
readonly deal: readonly ["name", "value", "stage"];
|
|
37
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* The concepts the mapping→sync pipeline can actually materialize into `crm_*`
|
|
41
|
+
* tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
|
|
42
|
+
* `persist()` throws for any other concept, so offering one (in the mapping
|
|
43
|
+
* editor, in suggestions, or via the upsert API) is a broken affordance.
|
|
44
|
+
*
|
|
45
|
+
* The broader CRM_CONCEPTS set still validates structurally, so previously
|
|
46
|
+
* stored rows for not-yet-materialized concepts keep rendering. When a concept
|
|
47
|
+
* gains a sync-store path, add its canonical fields above and it joins this set
|
|
48
|
+
* automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
|
|
49
|
+
*/
|
|
50
|
+
declare const MATERIALIZING_CONCEPTS: readonly ["contact", "company", "deal"];
|
|
51
|
+
type MaterializingConcept = (typeof MATERIALIZING_CONCEPTS)[number];
|
|
52
|
+
/**
|
|
53
|
+
* The org-defined custom-object target. Deliberately NOT a member of
|
|
54
|
+
* CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
|
|
55
|
+
* no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
|
|
56
|
+
* Handled as an explicit branch wherever concepts are switched on, so the
|
|
57
|
+
* canonical-concept invariants (and their drift test) stay intact.
|
|
58
|
+
*/
|
|
59
|
+
declare const CUSTOM_CONCEPT: "custom";
|
|
38
60
|
declare const filterValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodUnion<readonly [z.ZodObject<{
|
|
39
61
|
eq: z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
40
62
|
}, z.core.$strict>, z.ZodObject<{
|
|
@@ -126,14 +148,14 @@ declare const relationshipsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
|
126
148
|
}, z.core.$strict>>;
|
|
127
149
|
type RelationshipsMap = z.infer<typeof relationshipsSchema>;
|
|
128
150
|
declare const entitySchema: z.ZodObject<{
|
|
129
|
-
concept: z.ZodEnum<{
|
|
151
|
+
concept: z.ZodUnion<readonly [z.ZodEnum<{
|
|
130
152
|
contact: "contact";
|
|
131
153
|
company: "company";
|
|
132
154
|
deal: "deal";
|
|
133
155
|
activity: "activity";
|
|
134
156
|
conversation: "conversation";
|
|
135
157
|
note: "note";
|
|
136
|
-
}>;
|
|
158
|
+
}>, z.ZodLiteral<"custom">]>;
|
|
137
159
|
source: z.ZodString;
|
|
138
160
|
role: z.ZodOptional<z.ZodString>;
|
|
139
161
|
filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
@@ -169,18 +191,19 @@ declare const entitySchema: z.ZodObject<{
|
|
|
169
191
|
$raw: z.ZodString;
|
|
170
192
|
}, z.core.$strict>]>;
|
|
171
193
|
}, z.core.$strict>>>;
|
|
194
|
+
targetObjectTypeId: z.ZodOptional<z.ZodString>;
|
|
172
195
|
}, z.core.$strict>;
|
|
173
196
|
type ClivlyEntityConfig = z.infer<typeof entitySchema>;
|
|
174
197
|
declare const entitiesConfigSchema: z.ZodObject<{
|
|
175
198
|
entities: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
176
|
-
concept: z.ZodEnum<{
|
|
199
|
+
concept: z.ZodUnion<readonly [z.ZodEnum<{
|
|
177
200
|
contact: "contact";
|
|
178
201
|
company: "company";
|
|
179
202
|
deal: "deal";
|
|
180
203
|
activity: "activity";
|
|
181
204
|
conversation: "conversation";
|
|
182
205
|
note: "note";
|
|
183
|
-
}>;
|
|
206
|
+
}>, z.ZodLiteral<"custom">]>;
|
|
184
207
|
source: z.ZodString;
|
|
185
208
|
role: z.ZodOptional<z.ZodString>;
|
|
186
209
|
filter: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
@@ -216,6 +239,7 @@ declare const entitiesConfigSchema: z.ZodObject<{
|
|
|
216
239
|
$raw: z.ZodString;
|
|
217
240
|
}, z.core.$strict>]>;
|
|
218
241
|
}, z.core.$strict>>>;
|
|
242
|
+
targetObjectTypeId: z.ZodOptional<z.ZodString>;
|
|
219
243
|
}, z.core.$strict>>;
|
|
220
244
|
}, z.core.$strict>;
|
|
221
245
|
type ClivlyEntitiesConfig = z.infer<typeof entitiesConfigSchema>;
|
|
@@ -269,4 +293,4 @@ interface ConfigValidationResult {
|
|
|
269
293
|
*/
|
|
270
294
|
declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
|
|
271
295
|
//#endregion
|
|
272
|
-
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
|
296
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, ClivlyEntitiesConfig, ClivlyEntityConfig, ConfigValidationError, ConfigValidationResult, CrmConcept, DiscoveredSchemaTable, FilterExpr, FilterValue, MATERIALIZING_CONCEPTS, MaterializingConcept, RelationshipSpec, RelationshipVia, RelationshipsMap, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
package/dist/entity-config.mjs
CHANGED
|
@@ -48,9 +48,38 @@ const CANONICAL_FIELDS = {
|
|
|
48
48
|
"domain",
|
|
49
49
|
"industry",
|
|
50
50
|
"size"
|
|
51
|
+
],
|
|
52
|
+
deal: [
|
|
53
|
+
"name",
|
|
54
|
+
"value",
|
|
55
|
+
"stage"
|
|
51
56
|
]
|
|
52
57
|
};
|
|
53
58
|
/**
|
|
59
|
+
* The concepts the mapping→sync pipeline can actually materialize into `crm_*`
|
|
60
|
+
* tables. These are exactly the CANONICAL_FIELDS keys — the Drizzle sync store's
|
|
61
|
+
* `persist()` throws for any other concept, so offering one (in the mapping
|
|
62
|
+
* editor, in suggestions, or via the upsert API) is a broken affordance.
|
|
63
|
+
*
|
|
64
|
+
* The broader CRM_CONCEPTS set still validates structurally, so previously
|
|
65
|
+
* stored rows for not-yet-materialized concepts keep rendering. When a concept
|
|
66
|
+
* gains a sync-store path, add its canonical fields above and it joins this set
|
|
67
|
+
* automatically (guarded by the MATERIALIZING_CONCEPTS drift test).
|
|
68
|
+
*/
|
|
69
|
+
const MATERIALIZING_CONCEPTS = [
|
|
70
|
+
"contact",
|
|
71
|
+
"company",
|
|
72
|
+
"deal"
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* The org-defined custom-object target. Deliberately NOT a member of
|
|
76
|
+
* CRM_CONCEPTS / CANONICAL_FIELDS / MATERIALIZING_CONCEPTS — a custom object has
|
|
77
|
+
* no fixed canonical columns; its mapping's fieldMap keys ARE its properties.
|
|
78
|
+
* Handled as an explicit branch wherever concepts are switched on, so the
|
|
79
|
+
* canonical-concept invariants (and their drift test) stay intact.
|
|
80
|
+
*/
|
|
81
|
+
const CUSTOM_CONCEPT = "custom";
|
|
82
|
+
/**
|
|
54
83
|
* Operator forms for a single column predicate. All predicates on an entity are
|
|
55
84
|
* AND-ed together. `$raw` (see FilterExpr) is the escape hatch for anything the
|
|
56
85
|
* operators can't express.
|
|
@@ -124,8 +153,8 @@ const relationshipSchema = z.strictObject({
|
|
|
124
153
|
/** A named map of relationships, as stored on an entity / in the mappings row. */
|
|
125
154
|
const relationshipsSchema = z.record(z.string(), relationshipSchema);
|
|
126
155
|
const entitySchema = z.strictObject({
|
|
127
|
-
/** Which CRM concept this entity maps onto. */
|
|
128
|
-
concept: z.enum(CRM_CONCEPTS),
|
|
156
|
+
/** Which CRM concept this entity maps onto — a canonical concept or "custom". */
|
|
157
|
+
concept: z.union([z.enum(CRM_CONCEPTS), z.literal(CUSTOM_CONCEPT)]),
|
|
129
158
|
/** Host table (or view) this entity is derived from. */
|
|
130
159
|
source: z.string().min(1),
|
|
131
160
|
/**
|
|
@@ -143,7 +172,12 @@ const entitySchema = z.strictObject({
|
|
|
143
172
|
*/
|
|
144
173
|
readOnly: z.array(z.string()).optional(),
|
|
145
174
|
/** Named relationships to other entities in this config. */
|
|
146
|
-
relationships: z.record(z.string(), relationshipSchema).optional()
|
|
175
|
+
relationships: z.record(z.string(), relationshipSchema).optional(),
|
|
176
|
+
/**
|
|
177
|
+
* For a "custom" entity, the crm_object_types row this materializes into.
|
|
178
|
+
* Required for custom (enforced at the API layer), unused for standard concepts.
|
|
179
|
+
*/
|
|
180
|
+
targetObjectTypeId: z.string().min(1).optional()
|
|
147
181
|
});
|
|
148
182
|
const entitiesConfigSchema = z.strictObject({ entities: z.record(z.string(), entitySchema).refine((e) => Object.keys(e).length > 0, { message: "at least one entity must be defined" }) });
|
|
149
183
|
/** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
|
|
@@ -302,4 +336,4 @@ function validateEntitiesConfig(config, schema) {
|
|
|
302
336
|
};
|
|
303
337
|
}
|
|
304
338
|
//#endregion
|
|
305
|
-
export { CANONICAL_FIELDS, CRM_CONCEPTS, ClivlyConfigError, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
|
339
|
+
export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, MATERIALIZING_CONCEPTS, defineClivlyConfig, entitiesConfigSchema, filterSchema, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_entity_config = require("./entity-config.cjs");
|
|
3
|
+
const require_mapping_score = require("./mapping-score.cjs");
|
|
2
4
|
//#region src/entity-heuristics.ts
|
|
3
5
|
const TARGET_CONCEPTS = [
|
|
4
6
|
"contact",
|
|
@@ -116,7 +118,7 @@ const CONCEPT_FIELDS = {
|
|
|
116
118
|
{
|
|
117
119
|
field: "name",
|
|
118
120
|
candidates: ["name", "title"],
|
|
119
|
-
required:
|
|
121
|
+
required: true
|
|
120
122
|
},
|
|
121
123
|
{
|
|
122
124
|
field: "value",
|
|
@@ -208,12 +210,7 @@ function suggestCursorField(columnKeys) {
|
|
|
208
210
|
}
|
|
209
211
|
return null;
|
|
210
212
|
}
|
|
211
|
-
|
|
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
|
-
}
|
|
213
|
+
const MATERIALIZING = new Set(require_entity_config.MATERIALIZING_CONCEPTS);
|
|
217
214
|
function matchesConcept(tableName, concept) {
|
|
218
215
|
const normalized = tableName.toLowerCase();
|
|
219
216
|
const entry = TABLE_PATTERNS.find((p) => p.concept === concept);
|
|
@@ -222,19 +219,64 @@ function matchesConcept(tableName, concept) {
|
|
|
222
219
|
if (entry.names.some((name) => normalized.includes(name))) return { exact: false };
|
|
223
220
|
return null;
|
|
224
221
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
function metaFor(table, column) {
|
|
223
|
+
return table.columnsMeta?.find((c) => c.name === column);
|
|
224
|
+
}
|
|
225
|
+
function hasDefiningColumn(concept, columns) {
|
|
226
|
+
const lower = new Set(columns.map((c) => c.toLowerCase()));
|
|
227
|
+
const required = CONCEPT_FIELDS[concept].filter((f) => f.required);
|
|
228
|
+
return required.length > 0 && required.every((f) => f.candidates.some((c) => lower.has(c)));
|
|
229
|
+
}
|
|
230
|
+
function scoreConcepts(table) {
|
|
231
|
+
const out = [];
|
|
232
|
+
for (const concept of TARGET_CONCEPTS) {
|
|
233
|
+
if (!MATERIALIZING.has(concept)) continue;
|
|
234
|
+
const nameMatch = matchesConcept(table.name, concept);
|
|
235
|
+
if (!nameMatch) continue;
|
|
236
|
+
let score = 0;
|
|
237
|
+
const reasons = [];
|
|
238
|
+
if (nameMatch.exact) {
|
|
239
|
+
score += require_mapping_score.W.nameExact;
|
|
240
|
+
reasons.push(`name matches "${table.name}"`);
|
|
241
|
+
} else {
|
|
242
|
+
score += require_mapping_score.W.nameSubstring;
|
|
243
|
+
reasons.push(`name looks like ${concept}`);
|
|
233
244
|
}
|
|
245
|
+
if (hasDefiningColumn(concept, table.columns)) {
|
|
246
|
+
score += require_mapping_score.W.definingColumn;
|
|
247
|
+
reasons.push(`has the columns a ${concept} needs`);
|
|
248
|
+
}
|
|
249
|
+
if (table.columnsMeta?.some((c) => c.isPrimaryKey)) {
|
|
250
|
+
score += require_mapping_score.W.hasPrimaryKey;
|
|
251
|
+
reasons.push("has primary key");
|
|
252
|
+
}
|
|
253
|
+
if (table.rowCount !== void 0) if (table.rowCount === 0) {
|
|
254
|
+
score += require_mapping_score.W.emptyTable;
|
|
255
|
+
reasons.push("table is empty");
|
|
256
|
+
} else {
|
|
257
|
+
score += require_mapping_score.W.hasRows;
|
|
258
|
+
reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
|
|
259
|
+
}
|
|
260
|
+
out.push({
|
|
261
|
+
value: concept,
|
|
262
|
+
score,
|
|
263
|
+
tier: require_mapping_score.tierFor(score, require_mapping_score.CONCEPT_TIERS),
|
|
264
|
+
reasons
|
|
265
|
+
});
|
|
234
266
|
}
|
|
235
|
-
return
|
|
267
|
+
return out.sort((a, b) => b.score - a.score);
|
|
268
|
+
}
|
|
269
|
+
function topConcept(table) {
|
|
270
|
+
const ranked = scoreConcepts(table);
|
|
271
|
+
const best = ranked[0];
|
|
272
|
+
if (!best) return null;
|
|
273
|
+
return {
|
|
274
|
+
best,
|
|
275
|
+
ambiguous: require_mapping_score.isAmbiguous(ranked)
|
|
276
|
+
};
|
|
236
277
|
}
|
|
237
278
|
function requiredFieldsFor(concept) {
|
|
279
|
+
if (concept === "custom") return [];
|
|
238
280
|
return CONCEPT_FIELDS[concept].filter((f) => f.required).map((f) => f.field);
|
|
239
281
|
}
|
|
240
282
|
function validateMapping(requiredFields, fieldMap) {
|
|
@@ -262,14 +304,6 @@ function suggestDiscriminatorColumns(columns) {
|
|
|
262
304
|
}
|
|
263
305
|
return out;
|
|
264
306
|
}
|
|
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
307
|
const COMPANY_FK_HINTS = [
|
|
274
308
|
"organization_id",
|
|
275
309
|
"organisation_id",
|
|
@@ -287,58 +321,122 @@ function firstMatchingColumn(columns, hints) {
|
|
|
287
321
|
return null;
|
|
288
322
|
}
|
|
289
323
|
/**
|
|
290
|
-
* Suggest contact→company relationships
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
324
|
+
* Suggest contact→company relationships. When the source table carries FK
|
|
325
|
+
* metadata (`references`), the target table is a *fact*, not a name guess —
|
|
326
|
+
* that scores highest. Without metadata (v1 hosts) we fall back to the legacy
|
|
327
|
+
* FK-name hints at a lower tier.
|
|
294
328
|
*/
|
|
295
|
-
function
|
|
296
|
-
const companyTables = allTables.filter((t) => t.name !== source.name &&
|
|
329
|
+
function scoreRelationships(source, allTables) {
|
|
330
|
+
const companyTables = allTables.filter((t) => t.name !== source.name && topConcept(t)?.best.value === "company");
|
|
297
331
|
if (companyTables.length === 0) return [];
|
|
298
|
-
const
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
332
|
+
const companyNames = new Set(companyTables.map((t) => t.name.toLowerCase()));
|
|
333
|
+
const out = [];
|
|
334
|
+
const seenTargets = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const col of source.columnsMeta ?? []) {
|
|
336
|
+
const ref = col.references;
|
|
337
|
+
if (!(ref && companyNames.has(ref.table.toLowerCase()))) continue;
|
|
338
|
+
if (seenTargets.has(ref.table.toLowerCase())) continue;
|
|
339
|
+
seenTargets.add(ref.table.toLowerCase());
|
|
340
|
+
out.push({
|
|
341
|
+
value: {
|
|
342
|
+
name: "company",
|
|
343
|
+
entityTable: ref.table,
|
|
344
|
+
via: {
|
|
345
|
+
fkOn: "contact",
|
|
346
|
+
column: `${source.name}.${col.name}`
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
score: require_mapping_score.W.fkResolved,
|
|
350
|
+
tier: require_mapping_score.tierFor(require_mapping_score.W.fkResolved, require_mapping_score.RELATIONSHIP_TIERS),
|
|
351
|
+
reasons: [`foreign key → ${ref.table}.${ref.column}`]
|
|
317
352
|
});
|
|
318
353
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if (localKey && foreignKey) suggestions.push({
|
|
354
|
+
if (source.columnsMeta?.length) return out.sort((a, b) => b.score - a.score);
|
|
355
|
+
const contactFk = firstMatchingColumn(source.columns, COMPANY_FK_HINTS);
|
|
356
|
+
if (contactFk) out.push({
|
|
357
|
+
value: {
|
|
324
358
|
name: "company",
|
|
325
|
-
entityTable: companyTables
|
|
359
|
+
entityTable: companyTables[0]?.name ?? "",
|
|
326
360
|
via: {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
foreignKey
|
|
361
|
+
fkOn: "contact",
|
|
362
|
+
column: `${source.name}.${contactFk}`
|
|
330
363
|
}
|
|
364
|
+
},
|
|
365
|
+
score: require_mapping_score.W.fkNameHint,
|
|
366
|
+
tier: require_mapping_score.tierFor(require_mapping_score.W.fkNameHint, require_mapping_score.RELATIONSHIP_TIERS),
|
|
367
|
+
reasons: [`column "${contactFk}" looks like a company link`]
|
|
368
|
+
});
|
|
369
|
+
return out.sort((a, b) => b.score - a.score);
|
|
370
|
+
}
|
|
371
|
+
const FIELD_TYPE_EXPECTATION = {
|
|
372
|
+
name: "text",
|
|
373
|
+
email: "text",
|
|
374
|
+
domain: "text",
|
|
375
|
+
stage: "text",
|
|
376
|
+
status: "text",
|
|
377
|
+
subject: "text",
|
|
378
|
+
body: "text",
|
|
379
|
+
summary: "text",
|
|
380
|
+
type: "text",
|
|
381
|
+
value: "numeric",
|
|
382
|
+
createdAt: "timestamp"
|
|
383
|
+
};
|
|
384
|
+
function typeFits(field, type) {
|
|
385
|
+
const expectation = FIELD_TYPE_EXPECTATION[field];
|
|
386
|
+
if (!(expectation && type)) return null;
|
|
387
|
+
if (expectation === "text") return require_mapping_score.isTextLike(type);
|
|
388
|
+
if (expectation === "numeric") return require_mapping_score.isNumericLike(type);
|
|
389
|
+
return require_mapping_score.isTimestampLike(type);
|
|
390
|
+
}
|
|
391
|
+
function scoreFieldMap(concept, table) {
|
|
392
|
+
const lowerToActual = new Map(table.columns.map((c) => [c.toLowerCase(), c]));
|
|
393
|
+
const out = [];
|
|
394
|
+
for (const { field, candidates, required } of CONCEPT_FIELDS[concept]) for (const candidate of candidates) {
|
|
395
|
+
const column = lowerToActual.get(candidate);
|
|
396
|
+
if (!column) continue;
|
|
397
|
+
let score = require_mapping_score.W.fieldNameMatch;
|
|
398
|
+
const reasons = [`name matches "${column}"`];
|
|
399
|
+
const meta = metaFor(table, column);
|
|
400
|
+
const fits = typeFits(field, meta?.type);
|
|
401
|
+
if (fits === true) {
|
|
402
|
+
score += require_mapping_score.W.typeCompatible;
|
|
403
|
+
reasons.push(`type ${meta?.type}`);
|
|
404
|
+
} else if (fits === false) {
|
|
405
|
+
score += require_mapping_score.W.typeMismatch;
|
|
406
|
+
reasons.push(`type ${meta?.type} does not fit ${field}`);
|
|
407
|
+
}
|
|
408
|
+
if (required && meta?.nullable === false) {
|
|
409
|
+
score += require_mapping_score.W.requiredNotNull;
|
|
410
|
+
reasons.push("not null");
|
|
411
|
+
}
|
|
412
|
+
out.push({
|
|
413
|
+
value: {
|
|
414
|
+
field,
|
|
415
|
+
column,
|
|
416
|
+
required
|
|
417
|
+
},
|
|
418
|
+
score,
|
|
419
|
+
tier: require_mapping_score.tierFor(score, require_mapping_score.FIELD_TIERS),
|
|
420
|
+
reasons
|
|
331
421
|
});
|
|
422
|
+
break;
|
|
332
423
|
}
|
|
333
|
-
return
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function toFieldMap(scored) {
|
|
427
|
+
const map = {};
|
|
428
|
+
for (const { value } of scored) map[value.field] = value.column;
|
|
429
|
+
return map;
|
|
334
430
|
}
|
|
335
431
|
//#endregion
|
|
336
432
|
exports.TARGET_CONCEPTS = TARGET_CONCEPTS;
|
|
337
433
|
exports.matchesConcept = matchesConcept;
|
|
338
434
|
exports.requiredFieldsFor = requiredFieldsFor;
|
|
339
|
-
exports.
|
|
435
|
+
exports.scoreConcepts = scoreConcepts;
|
|
436
|
+
exports.scoreFieldMap = scoreFieldMap;
|
|
437
|
+
exports.scoreRelationships = scoreRelationships;
|
|
340
438
|
exports.suggestCursorField = suggestCursorField;
|
|
341
439
|
exports.suggestDiscriminatorColumns = suggestDiscriminatorColumns;
|
|
342
|
-
exports.
|
|
343
|
-
exports.
|
|
440
|
+
exports.toFieldMap = toFieldMap;
|
|
441
|
+
exports.topConcept = topConcept;
|
|
344
442
|
exports.validateMapping = validateMapping;
|