@clivly/core 0.1.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,35 @@
1
+ import { FilterExpr, RelationshipVia, RelationshipsMap } from "./entity-config.mjs";
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,47 @@
1
+ //#region src/mapping-form.ts
2
+ /**
3
+ * Build the filter for a discriminated entity: rows where `column` equals
4
+ * `value` belong to this entity (e.g. `role = "owner"`). Returns `undefined`
5
+ * when either side is blank so the caller can omit the filter entirely.
6
+ */
7
+ function buildFilterFromDiscriminator(column, value) {
8
+ const col = column?.trim();
9
+ const val = value.trim();
10
+ if (!col || val === "") return;
11
+ return { [col]: { eq: val } };
12
+ }
13
+ /**
14
+ * A stable identity string for a relationship's `via`. Two candidates on the
15
+ * same source table always differ in their join (`fkOn`+column, join-table
16
+ * keys, or raw SQL), so this disambiguates candidates the heuristic gives the
17
+ * same `name` (every contact→company candidate is named "company").
18
+ */
19
+ function relationshipViaSignature(via) {
20
+ if ("fkOn" in via) return `fk:${via.fkOn}:${via.column}`;
21
+ if ("through" in via) return `through:${via.through}:${via.localKey}:${via.foreignKey}`;
22
+ return `raw:${via.$raw}`;
23
+ }
24
+ /**
25
+ * Compile the selected relationship candidates into the persisted
26
+ * `RelationshipsMap`. Returns `undefined` for an empty selection so the caller
27
+ * omits the field.
28
+ */
29
+ function buildRelationships(candidates) {
30
+ if (candidates.length === 0) return;
31
+ const map = {};
32
+ for (const candidate of candidates) {
33
+ let key = candidate.name;
34
+ let suffix = 2;
35
+ while (key in map) {
36
+ key = `${candidate.name}_${suffix}`;
37
+ suffix++;
38
+ }
39
+ map[key] = {
40
+ entity: candidate.entityTable,
41
+ via: candidate.via
42
+ };
43
+ }
44
+ return map;
45
+ }
46
+ //#endregion
47
+ export { buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature };
@@ -0,0 +1,41 @@
1
+ import { ClivlyEntitiesConfig } from "./entity-config.mjs";
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 };
@@ -0,0 +1,79 @@
1
+ //#region src/mappings-config.ts
2
+ const DEFAULT_ROLE = "primary";
3
+ const ACTIVE_STATUS = "active";
4
+ function isEligible(row) {
5
+ return (row.status ?? ACTIVE_STATUS) === ACTIVE_STATUS && Object.keys(row.fieldMap ?? {}).length > 0;
6
+ }
7
+ /**
8
+ * Preferred key for a row: the bare source table when it backs a single entity,
9
+ * else role-suffixed so multiple roles on one table stay distinct and stable
10
+ * (the primary role keeps the bare name). Uniqueness is still enforced by the
11
+ * caller in case two rows land on the same preferred key.
12
+ */
13
+ function preferredKey(row, tableCounts) {
14
+ const role = row.role ?? DEFAULT_ROLE;
15
+ if ((tableCounts.get(row.sourceTable) ?? 0) <= 1) return row.sourceTable;
16
+ return role === DEFAULT_ROLE ? row.sourceTable : `${row.sourceTable}__${role}`;
17
+ }
18
+ function uniquify(base, used) {
19
+ if (!used.has(base)) {
20
+ used.add(base);
21
+ return base;
22
+ }
23
+ let suffix = 2;
24
+ while (used.has(`${base}__${suffix}`)) suffix++;
25
+ const key = `${base}__${suffix}`;
26
+ used.add(key);
27
+ return key;
28
+ }
29
+ function buildEntity(row, refIndex) {
30
+ const entity = {
31
+ concept: row.targetConcept,
32
+ source: row.sourceTable,
33
+ role: row.role ?? DEFAULT_ROLE,
34
+ fields: { ...row.fieldMap }
35
+ };
36
+ if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
37
+ if (row.relationships && Object.keys(row.relationships).length > 0) {
38
+ const relationships = {};
39
+ for (const [name, spec] of Object.entries(row.relationships)) {
40
+ const rel = spec;
41
+ relationships[name] = {
42
+ entity: refIndex.get(rel.entity) ?? rel.entity,
43
+ via: rel.via
44
+ };
45
+ }
46
+ entity.relationships = relationships;
47
+ }
48
+ return entity;
49
+ }
50
+ /**
51
+ * Compile persisted mapping rows into a `ClivlyEntitiesConfig`. Only `active`
52
+ * rows with at least one mapped field are included; disabled/suggested rows and
53
+ * unmapped rows are skipped. Returns `{ entities: {} }` when nothing is
54
+ * eligible — a no-op the view compiler and sync engine both tolerate.
55
+ */
56
+ function mappingsToEntitiesConfig(rows) {
57
+ const eligible = rows.filter(isEligible);
58
+ const tableCounts = /* @__PURE__ */ new Map();
59
+ for (const row of eligible) tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
60
+ const used = /* @__PURE__ */ new Set();
61
+ const tableAnchor = /* @__PURE__ */ new Map();
62
+ const assigned = [];
63
+ for (const row of eligible) {
64
+ const key = uniquify(preferredKey(row, tableCounts), used);
65
+ assigned.push({
66
+ row,
67
+ key
68
+ });
69
+ if ((row.role ?? DEFAULT_ROLE) === DEFAULT_ROLE || !tableAnchor.has(row.sourceTable)) tableAnchor.set(row.sourceTable, key);
70
+ }
71
+ const refIndex = /* @__PURE__ */ new Map();
72
+ for (const { key } of assigned) refIndex.set(key, key);
73
+ for (const [table, key] of tableAnchor) if (!refIndex.has(table)) refIndex.set(table, key);
74
+ const entities = {};
75
+ for (const { row, key } of assigned) entities[key] = buildEntity(row, refIndex);
76
+ return { entities };
77
+ }
78
+ //#endregion
79
+ export { mappingsToEntitiesConfig };
@@ -0,0 +1,138 @@
1
+ import { ClivlyEntitiesConfig } from "./entity-config.mjs";
2
+ import { CompileOptions, CompiledView } from "./view-compiler.mjs";
3
+
4
+ //#region src/sync-engine.d.ts
5
+ /**
6
+ * Sync engine (Phase 3) — materializes the read-only entity views into Clivly's
7
+ * own mirror tables (`crm_companies`, `crm_contacts`).
8
+ *
9
+ * The reconciliation is the heart of the design: rows present in a view are
10
+ * created or updated in the mirror; rows that *leave* a view (e.g. an owner
11
+ * demoted to a member) are **archived, not deleted**, so their deals/activities
12
+ * survive. Identity fields are host-owned and overwritten each run; Clivly-owned
13
+ * CRM fields are never touched here.
14
+ *
15
+ * `reconcile()` is a pure function and independently testable. `runSync()`
16
+ * orchestrates: compile views → for each (companies first, so contact
17
+ * `company_ref`s resolve to a live `crm_companies.id`) read the view, reconcile
18
+ * against the current mirror, resolve company refs, and persist. All DB I/O is
19
+ * behind the `SyncStore` port — core stays ORM-agnostic; the SDK/drizzle
20
+ * package provides the concrete store.
21
+ */
22
+ /** A row read from a compiled view (what Postgres returns). */
23
+ interface SourceRow {
24
+ /** Host-side company key from `<relationship>_ref`; resolved before persist. */
25
+ companyRef?: string | null;
26
+ /** Mapped identity fields (clivly field → value). */
27
+ fields: Record<string, string | null>;
28
+ /** The source primary key — becomes the mirror row's `source_ref`. */
29
+ sourceRef: string;
30
+ }
31
+ /** An existing mirror row, as far as reconciliation needs to know. */
32
+ interface MirrorRecord {
33
+ archivedAt: Date | null;
34
+ id: string;
35
+ sourceRef: string;
36
+ }
37
+ type SyncAction = {
38
+ kind: "create";
39
+ sourceRef: string;
40
+ fields: Record<string, string | null>;
41
+ companyRef: string | null;
42
+ } | {
43
+ kind: "update";
44
+ id: string;
45
+ sourceRef: string;
46
+ fields: Record<string, string | null>;
47
+ companyRef: string | null;
48
+ } | {
49
+ kind: "restore";
50
+ id: string;
51
+ sourceRef: string;
52
+ fields: Record<string, string | null>;
53
+ companyRef: string | null;
54
+ } | {
55
+ kind: "archive";
56
+ id: string;
57
+ sourceRef: string;
58
+ };
59
+ /** A persist action with the company ref resolved to a `crm_companies.id`. */
60
+ type PersistAction = {
61
+ kind: "create";
62
+ sourceRef: string;
63
+ fields: Record<string, string | null>;
64
+ companyId: string | null;
65
+ } | {
66
+ kind: "update";
67
+ id: string;
68
+ sourceRef: string;
69
+ fields: Record<string, string | null>;
70
+ companyId: string | null;
71
+ } | {
72
+ kind: "restore";
73
+ id: string;
74
+ sourceRef: string;
75
+ fields: Record<string, string | null>;
76
+ companyId: string | null;
77
+ } | {
78
+ kind: "archive";
79
+ id: string;
80
+ sourceRef: string;
81
+ };
82
+ /**
83
+ * Diff a view's rows against the current mirror population for one entity.
84
+ *
85
+ * - source row with no mirror row → create
86
+ * - source row with a live mirror row → update (overwrite identity fields)
87
+ * - source row with an archived mirror → restore (re-activate + update)
88
+ * - live mirror row absent from source → archive (soft delete, keep history)
89
+ * - archived mirror row absent from source → no-op
90
+ *
91
+ * Source rows are de-duplicated by `sourceRef` (first wins). A person appearing
92
+ * under multiple companies (many-to-many via a join table) collapses to their
93
+ * first company here; richer many-to-many membership is deferred.
94
+ */
95
+ declare function reconcile(source: SourceRow[], mirror: MirrorRecord[]): SyncAction[];
96
+ /** DB I/O port. The concrete store lives in the SDK/drizzle package. */
97
+ interface SyncStore {
98
+ /** Current mirror rows for a concept + role (including archived). */
99
+ listMirror(input: {
100
+ concept: string;
101
+ role: string;
102
+ }): Promise<MirrorRecord[]>;
103
+ /** Apply the resolved actions to the mirror table for this concept + role. */
104
+ persist(input: {
105
+ concept: string;
106
+ role: string;
107
+ actions: PersistAction[];
108
+ }): Promise<void>;
109
+ /** Execute the view's SQL and return its rows. */
110
+ readView(view: CompiledView): Promise<SourceRow[]>;
111
+ /** Map host-side company refs → `crm_companies.id` (already-synced companies only). */
112
+ resolveCompanyRefs(refs: string[]): Promise<Map<string, string>>;
113
+ }
114
+ interface EntitySyncResult {
115
+ archived: number;
116
+ concept: string;
117
+ created: number;
118
+ entityKey: string;
119
+ restored: number;
120
+ role: string;
121
+ updated: number;
122
+ }
123
+ interface SyncResult {
124
+ entities: EntitySyncResult[];
125
+ totals: {
126
+ created: number;
127
+ updated: number;
128
+ restored: number;
129
+ archived: number;
130
+ };
131
+ }
132
+ /**
133
+ * Run a full sync: compile the config's views, then reconcile + persist each,
134
+ * companies first. Returns per-entity and total action counts.
135
+ */
136
+ declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
137
+ //#endregion
138
+ export { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync };
@@ -0,0 +1,138 @@
1
+ import { compileEntityViews } from "./view-compiler.mjs";
2
+ //#region src/sync-engine.ts
3
+ /**
4
+ * Diff a view's rows against the current mirror population for one entity.
5
+ *
6
+ * - source row with no mirror row → create
7
+ * - source row with a live mirror row → update (overwrite identity fields)
8
+ * - source row with an archived mirror → restore (re-activate + update)
9
+ * - live mirror row absent from source → archive (soft delete, keep history)
10
+ * - archived mirror row absent from source → no-op
11
+ *
12
+ * Source rows are de-duplicated by `sourceRef` (first wins). A person appearing
13
+ * under multiple companies (many-to-many via a join table) collapses to their
14
+ * first company here; richer many-to-many membership is deferred.
15
+ */
16
+ function reconcile(source, mirror) {
17
+ const seen = /* @__PURE__ */ new Set();
18
+ const rows = [];
19
+ for (const row of source) if (!seen.has(row.sourceRef)) {
20
+ seen.add(row.sourceRef);
21
+ rows.push(row);
22
+ }
23
+ const byRef = new Map(mirror.map((m) => [m.sourceRef, m]));
24
+ const liveRefs = /* @__PURE__ */ new Set();
25
+ const actions = [];
26
+ for (const row of rows) {
27
+ liveRefs.add(row.sourceRef);
28
+ const existing = byRef.get(row.sourceRef);
29
+ const companyRef = row.companyRef ?? null;
30
+ if (!existing) actions.push({
31
+ kind: "create",
32
+ sourceRef: row.sourceRef,
33
+ fields: row.fields,
34
+ companyRef
35
+ });
36
+ else if (existing.archivedAt) actions.push({
37
+ kind: "restore",
38
+ id: existing.id,
39
+ sourceRef: row.sourceRef,
40
+ fields: row.fields,
41
+ companyRef
42
+ });
43
+ else actions.push({
44
+ kind: "update",
45
+ id: existing.id,
46
+ sourceRef: row.sourceRef,
47
+ fields: row.fields,
48
+ companyRef
49
+ });
50
+ }
51
+ for (const m of mirror) if (!(liveRefs.has(m.sourceRef) || m.archivedAt)) actions.push({
52
+ kind: "archive",
53
+ id: m.id,
54
+ sourceRef: m.sourceRef
55
+ });
56
+ return actions;
57
+ }
58
+ const CONCEPT_ORDER = {
59
+ company: 0,
60
+ contact: 1
61
+ };
62
+ /** Companies before contacts, so contact company_refs resolve to a live mirror. */
63
+ function syncOrder(a, b) {
64
+ return (CONCEPT_ORDER[a.concept] ?? 99) - (CONCEPT_ORDER[b.concept] ?? 99);
65
+ }
66
+ async function resolveActions(store, actions) {
67
+ const refs = /* @__PURE__ */ new Set();
68
+ for (const action of actions) if (action.kind !== "archive" && action.companyRef) refs.add(action.companyRef);
69
+ const resolved = refs.size ? await store.resolveCompanyRefs([...refs]) : /* @__PURE__ */ new Map();
70
+ return actions.map((action) => {
71
+ if (action.kind === "archive") return action;
72
+ const companyId = action.companyRef ? resolved.get(action.companyRef) ?? null : null;
73
+ return {
74
+ kind: action.kind,
75
+ id: "id" in action ? action.id : "",
76
+ sourceRef: action.sourceRef,
77
+ fields: action.fields,
78
+ companyId
79
+ };
80
+ });
81
+ }
82
+ function emptyTally() {
83
+ return {
84
+ create: 0,
85
+ update: 0,
86
+ restore: 0,
87
+ archive: 0
88
+ };
89
+ }
90
+ /**
91
+ * Run a full sync: compile the config's views, then reconcile + persist each,
92
+ * companies first. Returns per-entity and total action counts.
93
+ */
94
+ async function runSync(config, store, options = {}) {
95
+ const views = compileEntityViews(config, options).sort(syncOrder);
96
+ const entities = [];
97
+ const totals = {
98
+ created: 0,
99
+ updated: 0,
100
+ restored: 0,
101
+ archived: 0
102
+ };
103
+ for (const view of views) {
104
+ const [source, mirror] = await Promise.all([store.readView(view), store.listMirror({
105
+ concept: view.concept,
106
+ role: view.role
107
+ })]);
108
+ const actions = reconcile(source, mirror);
109
+ const persistActions = await resolveActions(store, actions);
110
+ await store.persist({
111
+ concept: view.concept,
112
+ role: view.role,
113
+ actions: persistActions
114
+ });
115
+ const counts = emptyTally();
116
+ for (const action of actions) counts[action.kind] += 1;
117
+ const result = {
118
+ entityKey: view.entityKey,
119
+ concept: view.concept,
120
+ role: view.role,
121
+ created: counts.create,
122
+ updated: counts.update,
123
+ restored: counts.restore,
124
+ archived: counts.archive
125
+ };
126
+ entities.push(result);
127
+ totals.created += result.created;
128
+ totals.updated += result.updated;
129
+ totals.restored += result.restored;
130
+ totals.archived += result.archived;
131
+ }
132
+ return {
133
+ entities,
134
+ totals
135
+ };
136
+ }
137
+ //#endregion
138
+ export { reconcile, runSync };
@@ -0,0 +1,148 @@
1
+ //#region src/types.d.ts
2
+ type ContactStatus = "lead" | "active" | "customer" | "churned";
3
+ type DealStage = "lead" | "qualified" | "proposal" | "negotiation" | "won" | "lost";
4
+ type TaskStatus = "todo" | "in_progress" | "done" | "cancelled";
5
+ type TaskPriority = "low" | "medium" | "high";
6
+ type TaskEntityType = "contact" | "company" | "deal";
7
+ type ActivityType = "note" | "email" | "task" | "deal" | "call";
8
+ type CrmRole = "owner" | "admin" | "member" | "developer" | "support_agent";
9
+ interface Contact {
10
+ companyId: string | null;
11
+ createdAt: Date;
12
+ email: string;
13
+ id: string;
14
+ lastActivityAt: Date | null;
15
+ name: string;
16
+ orgId: string;
17
+ status: ContactStatus;
18
+ title: string | null;
19
+ }
20
+ interface Company {
21
+ createdAt: Date;
22
+ domain: string | null;
23
+ id: string;
24
+ industry: string | null;
25
+ name: string;
26
+ orgId: string;
27
+ size: string | null;
28
+ }
29
+ interface Deal {
30
+ closeDate: Date | null;
31
+ contactId: string | null;
32
+ createdAt: Date;
33
+ id: string;
34
+ name: string;
35
+ orgId: string;
36
+ owner: string | null;
37
+ stage: DealStage;
38
+ value: number;
39
+ }
40
+ interface Task {
41
+ archivedAt: Date | null;
42
+ assignee: string | null;
43
+ completedAt: Date | null;
44
+ contactId: string | null;
45
+ createdAt: Date;
46
+ createdBy: string | null;
47
+ description: string | null;
48
+ dueAt: Date | null;
49
+ entityId: string | null;
50
+ entityType: TaskEntityType | null;
51
+ id: string;
52
+ orgId: string;
53
+ priority: TaskPriority;
54
+ status: TaskStatus;
55
+ title: string;
56
+ }
57
+ interface Note {
58
+ archivedAt: Date | null;
59
+ author: string | null;
60
+ body: string;
61
+ contactId: string | null;
62
+ createdAt: Date;
63
+ createdBy: string | null;
64
+ entityId: string | null;
65
+ entityType: TaskEntityType | null;
66
+ id: string;
67
+ isTemplate: boolean;
68
+ orgId: string;
69
+ title: string;
70
+ updatedAt: Date;
71
+ }
72
+ interface Activity {
73
+ actor: string | null;
74
+ contactId: string | null;
75
+ createdAt: Date;
76
+ id: string;
77
+ orgId: string;
78
+ summary: string;
79
+ type: ActivityType;
80
+ }
81
+ interface CreateContactInput {
82
+ companyId?: string;
83
+ email: string;
84
+ name: string;
85
+ status?: ContactStatus;
86
+ title?: string;
87
+ }
88
+ interface UpdateContactInput {
89
+ companyId?: string | null;
90
+ email?: string;
91
+ name?: string;
92
+ status?: ContactStatus;
93
+ title?: string | null;
94
+ }
95
+ interface CreateCompanyInput {
96
+ domain?: string;
97
+ industry?: string;
98
+ name: string;
99
+ size?: string;
100
+ }
101
+ interface CreateDealInput {
102
+ contactId?: string;
103
+ name: string;
104
+ owner?: string;
105
+ stage?: DealStage;
106
+ value?: number;
107
+ }
108
+ interface CreateTaskInput {
109
+ assignee?: string;
110
+ createdBy?: string;
111
+ description?: string;
112
+ dueAt?: Date;
113
+ entityId?: string;
114
+ entityType?: TaskEntityType;
115
+ priority?: TaskPriority;
116
+ title: string;
117
+ }
118
+ interface UpdateTaskInput {
119
+ assignee?: string | null;
120
+ description?: string | null;
121
+ dueAt?: Date | null;
122
+ entityId?: string | null;
123
+ entityType?: TaskEntityType | null;
124
+ priority?: TaskPriority;
125
+ title?: string;
126
+ }
127
+ interface CreateActivityInput {
128
+ actor?: string;
129
+ contactId?: string;
130
+ summary: string;
131
+ type: ActivityType;
132
+ }
133
+ interface ContactFilters {
134
+ companyId?: string;
135
+ search?: string;
136
+ status?: ContactStatus;
137
+ }
138
+ interface TaskFilters {
139
+ assignee?: string;
140
+ entityId?: string;
141
+ entityType?: TaskEntityType;
142
+ includeArchived?: boolean;
143
+ includeCompleted?: boolean;
144
+ priority?: TaskPriority;
145
+ status?: TaskStatus;
146
+ }
147
+ //#endregion
148
+ export { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateTaskInput };
package/dist/types.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { ClivlyEntitiesConfig } from "./entity-config.mjs";
2
+
3
+ //#region src/view-compiler.d.ts
4
+ interface CompileOptions {
5
+ /**
6
+ * Optional tenant-scoping predicate ANDed into every view's WHERE, e.g.
7
+ * `{ column: "workspace_id", value: "ws_123" }`. Column must exist on the
8
+ * entity's source table (the compiler does not verify that — validate first).
9
+ */
10
+ orgScope?: {
11
+ column: string;
12
+ value: string | number;
13
+ };
14
+ /**
15
+ * Emit views `WITH (security_invoker = true)` so they run with the querying
16
+ * role's privileges instead of the (owner's) definer privileges — the
17
+ * default Postgres behaviour, which bypasses the caller's RLS. Defaults to
18
+ * `true`: the security boundary the product promises. Set `false` only for
19
+ * hosts on Postgres < 15, which lacks the option.
20
+ */
21
+ securityInvoker?: boolean;
22
+ /** View name prefix. Defaults to `crm_src_`. */
23
+ viewPrefix?: string;
24
+ }
25
+ interface CompiledView {
26
+ /** Output column names in SELECT order (`id`, mapped fields, then `<rel>_ref`s). */
27
+ columns: string[];
28
+ concept: string;
29
+ /** `DROP VIEW IF EXISTS …` statement (for teardown / reconfiguration). */
30
+ dropSql: string;
31
+ entityKey: string;
32
+ /** Resolved role — the entity's `role` or `"primary"`. */
33
+ role: string;
34
+ sourceTable: string;
35
+ /** `CREATE OR REPLACE VIEW … AS …` statement. */
36
+ sql: string;
37
+ viewName: string;
38
+ }
39
+ declare function compileEntityView(config: ClivlyEntitiesConfig, entityKey: string, options?: CompileOptions): CompiledView;
40
+ /** Compile every entity in the config into its view, in declaration order. */
41
+ declare function compileEntityViews(config: ClivlyEntitiesConfig, options?: CompileOptions): CompiledView[];
42
+ //#endregion
43
+ export { CompileOptions, CompiledView, compileEntityView, compileEntityViews };