@clivly/core 0.2.0 → 0.3.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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): Promise<Company[]>;
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): Promise<Deal[]>;
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>;
@@ -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): Promise<Company[]>;
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): Promise<Deal[]>;
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>;
@@ -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;
@@ -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 };
@@ -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 };
@@ -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: false
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
- 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
- }
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 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;
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 map;
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 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.
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 suggestRelationships(source, allTables) {
296
- const companyTables = allTables.filter((t) => t.name !== source.name && suggestConcept(t.name) === "company");
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 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
- }
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
- 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({
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.find((c) => c.name !== table.name)?.name ?? companyTables[0]?.name ?? "",
359
+ entityTable: companyTables[0]?.name ?? "",
326
360
  via: {
327
- through: table.name,
328
- localKey,
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 suggestions;
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.suggestConcept = suggestConcept;
435
+ exports.scoreConcepts = scoreConcepts;
436
+ exports.scoreFieldMap = scoreFieldMap;
437
+ exports.scoreRelationships = scoreRelationships;
340
438
  exports.suggestCursorField = suggestCursorField;
341
439
  exports.suggestDiscriminatorColumns = suggestDiscriminatorColumns;
342
- exports.suggestFieldMap = suggestFieldMap;
343
- exports.suggestRelationships = suggestRelationships;
440
+ exports.toFieldMap = toFieldMap;
441
+ exports.topConcept = topConcept;
344
442
  exports.validateMapping = validateMapping;