@clivly/core 0.14.1 → 0.15.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.
- package/README.md +1 -1
- package/dist/adapter.d.cts +14 -0
- package/dist/adapter.d.mts +14 -0
- package/dist/index.cjs +12 -0
- package/dist/index.d.cts +3 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +12 -0
- package/dist/integration-state.cjs +1 -0
- package/dist/integration-state.d.cts +1 -0
- package/dist/integration-state.d.mts +1 -0
- package/dist/integration-state.mjs +1 -1
- package/dist/types.d.cts +35 -0
- package/dist/types.d.mts +35 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ client.
|
|
|
48
48
|
| Entry | Contents |
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, projections, shared types |
|
|
51
|
-
| `@clivly/core/adapter` | `CRMAdapter` — the interface every ORM implementation fulfils (org-scoped; contacts, companies, deals, tasks, activities). List reads are keyset-paged: `listContactsPage`, `listCompaniesPage`, `listDealsPage` and `listTasksPage` take a `PageRequest` and return a `Page` |
|
|
51
|
+
| `@clivly/core/adapter` | `CRMAdapter` — the interface every ORM implementation fulfils (org-scoped; contacts, companies, deals, tasks, activities). List reads are keyset-paged: `listContactsPage`, `listCompaniesPage`, `listDealsPage` and `listTasksPage` take a `PageRequest` and return a `Page`; `countContacts` returns how many contacts match the same `ContactFilters` |
|
|
52
52
|
| `@clivly/core/auth-adapter` | `ClivlyAuthAdapter`, `ClivlyUser`, `authCustom()`, `MembershipResolver`, `CrmMembership` — pluggable host-app identity + the `crm_members` authorization layer |
|
|
53
53
|
| `@clivly/core/entity-config` | `defineClivlyConfig`, `validateEntitiesConfig`, `entitiesConfigSchema`, `CANONICAL_FIELDS`, `CRM_CONCEPTS`, `ClivlyConfigError`, and the config types |
|
|
54
54
|
| `@clivly/core/types` | Shared domain types (`Contact`, `Company`, `Deal`, `Task`, `Note`, `Activity`, inputs, filters, `CrmRole`) |
|
package/dist/adapter.d.cts
CHANGED
|
@@ -19,6 +19,20 @@ export interface CRMAdapter {
|
|
|
19
19
|
archiveContact(orgId: string, id: string): Promise<Contact>;
|
|
20
20
|
archiveTask(orgId: string, id: string): Promise<Task>;
|
|
21
21
|
completeTask(orgId: string, id: string): Promise<Task>;
|
|
22
|
+
/**
|
|
23
|
+
* How many contacts match `filters`, ignoring pagination entirely.
|
|
24
|
+
*
|
|
25
|
+
* Exists so a caller can state the size of a selection before acting on it —
|
|
26
|
+
* a bulk send has to say "this will go to N contacts", and the paged read
|
|
27
|
+
* cannot answer that: it knows only whether *another* page exists, never how
|
|
28
|
+
* many rows are behind it.
|
|
29
|
+
*
|
|
30
|
+
* Takes the same `ContactFilters` as {@link CRMAdapter.listContactsPage} and
|
|
31
|
+
* applies exactly the same predicates, so the count and the list can never
|
|
32
|
+
* disagree about what matches. It returns a number rather than rows, so it
|
|
33
|
+
* stays bounded however large the match is.
|
|
34
|
+
*/
|
|
35
|
+
countContacts(orgId: string, filters?: ContactFilters): Promise<number>;
|
|
22
36
|
createActivity(orgId: string, data: CreateActivityInput): Promise<Activity>;
|
|
23
37
|
createCompany(orgId: string, data: CreateCompanyInput): Promise<Company>;
|
|
24
38
|
createContact(orgId: string, data: CreateContactInput): Promise<Contact>;
|
package/dist/adapter.d.mts
CHANGED
|
@@ -19,6 +19,20 @@ export interface CRMAdapter {
|
|
|
19
19
|
archiveContact(orgId: string, id: string): Promise<Contact>;
|
|
20
20
|
archiveTask(orgId: string, id: string): Promise<Task>;
|
|
21
21
|
completeTask(orgId: string, id: string): Promise<Task>;
|
|
22
|
+
/**
|
|
23
|
+
* How many contacts match `filters`, ignoring pagination entirely.
|
|
24
|
+
*
|
|
25
|
+
* Exists so a caller can state the size of a selection before acting on it —
|
|
26
|
+
* a bulk send has to say "this will go to N contacts", and the paged read
|
|
27
|
+
* cannot answer that: it knows only whether *another* page exists, never how
|
|
28
|
+
* many rows are behind it.
|
|
29
|
+
*
|
|
30
|
+
* Takes the same `ContactFilters` as {@link CRMAdapter.listContactsPage} and
|
|
31
|
+
* applies exactly the same predicates, so the count and the list can never
|
|
32
|
+
* disagree about what matches. It returns a number rather than rows, so it
|
|
33
|
+
* stays bounded however large the match is.
|
|
34
|
+
*/
|
|
35
|
+
countContacts(orgId: string, filters?: ContactFilters): Promise<number>;
|
|
22
36
|
createActivity(orgId: string, data: CreateActivityInput): Promise<Activity>;
|
|
23
37
|
createCompany(orgId: string, data: CreateCompanyInput): Promise<Company>;
|
|
24
38
|
createContact(orgId: string, data: CreateContactInput): Promise<Contact>;
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,18 @@ function computeDealValue(source, fields, mappedFields, stageType) {
|
|
|
33
33
|
const seats = Number(rawSeats);
|
|
34
34
|
if (!Number.isSafeInteger(seats)) return null;
|
|
35
35
|
const monthly = (source.baseFeeCents ?? 0) + seats * unitPriceCents;
|
|
36
|
+
if (!Number.isSafeInteger(monthly) || monthly < 0) return null;
|
|
37
|
+
return normalizePrice(monthly, source, fields, mappedFields);
|
|
38
|
+
}
|
|
39
|
+
/** Preserve old configurations while making new price bases explicit. */
|
|
40
|
+
function normalizePrice(monthly, source, fields, mappedFields) {
|
|
41
|
+
if (source.priceBasis === "monthly") return monthly;
|
|
42
|
+
if (source.priceBasis === "billing_interval") {
|
|
43
|
+
const interval = source.intervalField && mappedFields.has(source.intervalField) ? fields[source.intervalField]?.trim().toLowerCase() : null;
|
|
44
|
+
if (interval === "month" || interval === "monthly") return monthly;
|
|
45
|
+
if (interval === "year" || interval === "annual" || interval === "annually" || interval === "yearly") return Math.round(monthly / MONTHS_PER_YEAR);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
36
48
|
return source.intervalField !== void 0 && mappedFields.has(source.intervalField) && fields[source.intervalField] === "year" ? Math.round(monthly / MONTHS_PER_YEAR) : monthly;
|
|
37
49
|
}
|
|
38
50
|
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -17,7 +17,7 @@ import { EntitySyncResult, MirrorRecord, PersistAction, ResolvedMembership, Sour
|
|
|
17
17
|
* D8 made for stage-rule conditions.
|
|
18
18
|
*/
|
|
19
19
|
interface PerSeatValueSource {
|
|
20
|
-
/** Flat
|
|
20
|
+
/** Flat fee in cents, using the same price basis as the plan prices. */
|
|
21
21
|
baseFeeCents?: number;
|
|
22
22
|
/** Mapped column holding "month" or "year". Absent means always monthly. */
|
|
23
23
|
intervalField?: string;
|
|
@@ -26,6 +26,8 @@ interface PerSeatValueSource {
|
|
|
26
26
|
planField: string;
|
|
27
27
|
/** Unit price in cents, keyed by the value found in `planField`. */
|
|
28
28
|
plans: Record<string, number>;
|
|
29
|
+
/** Monthly rates or amounts charged each billing period. Omitted preserves legacy behaviour. */
|
|
30
|
+
priceBasis?: "monthly" | "billing_interval";
|
|
29
31
|
/** Mapped column holding the seat count. */
|
|
30
32
|
seatField: string;
|
|
31
33
|
/** Stage types whose deals are worth nothing (usually `["lost"]`). */
|
package/dist/index.d.mts
CHANGED
|
@@ -17,7 +17,7 @@ import { EntitySyncResult, MirrorRecord, PersistAction, ResolvedMembership, Sour
|
|
|
17
17
|
* D8 made for stage-rule conditions.
|
|
18
18
|
*/
|
|
19
19
|
interface PerSeatValueSource {
|
|
20
|
-
/** Flat
|
|
20
|
+
/** Flat fee in cents, using the same price basis as the plan prices. */
|
|
21
21
|
baseFeeCents?: number;
|
|
22
22
|
/** Mapped column holding "month" or "year". Absent means always monthly. */
|
|
23
23
|
intervalField?: string;
|
|
@@ -26,6 +26,8 @@ interface PerSeatValueSource {
|
|
|
26
26
|
planField: string;
|
|
27
27
|
/** Unit price in cents, keyed by the value found in `planField`. */
|
|
28
28
|
plans: Record<string, number>;
|
|
29
|
+
/** Monthly rates or amounts charged each billing period. Omitted preserves legacy behaviour. */
|
|
30
|
+
priceBasis?: "monthly" | "billing_interval";
|
|
29
31
|
/** Mapped column holding the seat count. */
|
|
30
32
|
seatField: string;
|
|
31
33
|
/** Stage types whose deals are worth nothing (usually `["lost"]`). */
|
package/dist/index.mjs
CHANGED
|
@@ -32,6 +32,18 @@ function computeDealValue(source, fields, mappedFields, stageType) {
|
|
|
32
32
|
const seats = Number(rawSeats);
|
|
33
33
|
if (!Number.isSafeInteger(seats)) return null;
|
|
34
34
|
const monthly = (source.baseFeeCents ?? 0) + seats * unitPriceCents;
|
|
35
|
+
if (!Number.isSafeInteger(monthly) || monthly < 0) return null;
|
|
36
|
+
return normalizePrice(monthly, source, fields, mappedFields);
|
|
37
|
+
}
|
|
38
|
+
/** Preserve old configurations while making new price bases explicit. */
|
|
39
|
+
function normalizePrice(monthly, source, fields, mappedFields) {
|
|
40
|
+
if (source.priceBasis === "monthly") return monthly;
|
|
41
|
+
if (source.priceBasis === "billing_interval") {
|
|
42
|
+
const interval = source.intervalField && mappedFields.has(source.intervalField) ? fields[source.intervalField]?.trim().toLowerCase() : null;
|
|
43
|
+
if (interval === "month" || interval === "monthly") return monthly;
|
|
44
|
+
if (interval === "year" || interval === "annual" || interval === "annually" || interval === "yearly") return Math.round(monthly / MONTHS_PER_YEAR);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
35
47
|
return source.intervalField !== void 0 && mappedFields.has(source.intervalField) && fields[source.intervalField] === "year" ? Math.round(monthly / MONTHS_PER_YEAR) : monthly;
|
|
36
48
|
}
|
|
37
49
|
//#endregion
|
|
@@ -346,6 +346,7 @@ exports.DEFAULT_HEARTBEAT_INTERVAL_MS = DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
|
346
346
|
exports.INSTALL_COMMAND = INSTALL_COMMAND;
|
|
347
347
|
exports.INTEGRATION_STEPS = INTEGRATION_STEPS;
|
|
348
348
|
exports.INTEGRATION_STEP_LABELS = INTEGRATION_STEP_LABELS;
|
|
349
|
+
exports.LIVE_INTERVALS = LIVE_INTERVALS;
|
|
349
350
|
exports.STEP_PREREQUISITES = STEP_PREREQUISITES;
|
|
350
351
|
exports.computeIntegrationState = computeIntegrationState;
|
|
351
352
|
exports.deriveLiveness = deriveLiveness;
|
|
@@ -111,6 +111,7 @@ export interface IntegrationStep {
|
|
|
111
111
|
export declare function formatAge(ms: number): string;
|
|
112
112
|
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
113
113
|
export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
114
|
+
export declare const LIVE_INTERVALS = 2.5;
|
|
114
115
|
export type ConnectionHealth = "live" | "live_with_issues" | "stale" | "offline";
|
|
115
116
|
/**
|
|
116
117
|
* Clivly's own answer to "is this app up", obtained by asking it rather than
|
|
@@ -111,6 +111,7 @@ export interface IntegrationStep {
|
|
|
111
111
|
export declare function formatAge(ms: number): string;
|
|
112
112
|
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
113
113
|
export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
114
|
+
export declare const LIVE_INTERVALS = 2.5;
|
|
114
115
|
export type ConnectionHealth = "live" | "live_with_issues" | "stale" | "offline";
|
|
115
116
|
/**
|
|
116
117
|
* Clivly's own answer to "is this app up", obtained by asking it rather than
|
|
@@ -341,4 +341,4 @@ function computeIntegrationState(facts) {
|
|
|
341
341
|
};
|
|
342
342
|
}
|
|
343
343
|
//#endregion
|
|
344
|
-
export { DEFAULT_HEARTBEAT_INTERVAL_MS, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, STEP_PREREQUISITES, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
344
|
+
export { DEFAULT_HEARTBEAT_INTERVAL_MS, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, LIVE_INTERVALS, STEP_PREREQUISITES, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
package/dist/types.d.cts
CHANGED
|
@@ -84,12 +84,21 @@ export interface Company {
|
|
|
84
84
|
}
|
|
85
85
|
export interface Deal {
|
|
86
86
|
closeDate: Date | null;
|
|
87
|
+
/** The organisation this deal belongs to, when one is linked. */
|
|
88
|
+
companyId: string | null;
|
|
87
89
|
contactId: string | null;
|
|
88
90
|
createdAt: Date;
|
|
89
91
|
id: string;
|
|
90
92
|
name: string;
|
|
91
93
|
orgId: string;
|
|
92
94
|
owner: string | null;
|
|
95
|
+
/**
|
|
96
|
+
* The board this deal belongs to. NULL means the org's default pipeline: the
|
|
97
|
+
* sync mirror wrote deals before pipelines existed, and a deal created with
|
|
98
|
+
* no pipeline lands on the default. Stage keys are unique per pipeline, so
|
|
99
|
+
* a board must filter on this, never on stage key alone.
|
|
100
|
+
*/
|
|
101
|
+
pipelineId: string | null;
|
|
93
102
|
position: number | null;
|
|
94
103
|
stage: DealStage;
|
|
95
104
|
/**
|
|
@@ -116,6 +125,11 @@ export interface Task {
|
|
|
116
125
|
createdAt: Date;
|
|
117
126
|
createdBy: string | null;
|
|
118
127
|
description: string | null;
|
|
128
|
+
/**
|
|
129
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
130
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
131
|
+
*/
|
|
132
|
+
dueAllDay?: boolean | null;
|
|
119
133
|
dueAt: Date | null;
|
|
120
134
|
entityId: string | null;
|
|
121
135
|
entityType: TaskEntityType | null;
|
|
@@ -203,6 +217,11 @@ export interface CreateTaskInput {
|
|
|
203
217
|
assignee?: string;
|
|
204
218
|
createdBy?: string;
|
|
205
219
|
description?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
222
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
223
|
+
*/
|
|
224
|
+
dueAllDay?: boolean | null;
|
|
206
225
|
dueAt?: Date;
|
|
207
226
|
entityId?: string;
|
|
208
227
|
entityType?: TaskEntityType;
|
|
@@ -212,6 +231,11 @@ export interface CreateTaskInput {
|
|
|
212
231
|
export interface UpdateTaskInput {
|
|
213
232
|
assignee?: string | null;
|
|
214
233
|
description?: string | null;
|
|
234
|
+
/**
|
|
235
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
236
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
237
|
+
*/
|
|
238
|
+
dueAllDay?: boolean | null;
|
|
215
239
|
dueAt?: Date | null;
|
|
216
240
|
entityId?: string | null;
|
|
217
241
|
entityType?: TaskEntityType | null;
|
|
@@ -244,6 +268,17 @@ export interface ContactFilters {
|
|
|
244
268
|
* row is no longer sync-provenanced.
|
|
245
269
|
*/
|
|
246
270
|
excludeArchived?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Case-insensitive substring match on name or email.
|
|
273
|
+
*
|
|
274
|
+
* Applied in SQL, not over an already-fetched page: a filter applied after
|
|
275
|
+
* paging returns a partial page and a cursor that skips the rows it dropped,
|
|
276
|
+
* so "page 2" of a search is not the second page of the matches. An empty or
|
|
277
|
+
* whitespace-only value is ignored rather than matching nothing.
|
|
278
|
+
*
|
|
279
|
+
* LIKE metacharacters in the term are escaped and matched literally — a
|
|
280
|
+
* search for `%` finds contacts containing a percent sign, not every row.
|
|
281
|
+
*/
|
|
247
282
|
search?: string;
|
|
248
283
|
source?: RecordSource;
|
|
249
284
|
status?: ContactStatus;
|
package/dist/types.d.mts
CHANGED
|
@@ -84,12 +84,21 @@ export interface Company {
|
|
|
84
84
|
}
|
|
85
85
|
export interface Deal {
|
|
86
86
|
closeDate: Date | null;
|
|
87
|
+
/** The organisation this deal belongs to, when one is linked. */
|
|
88
|
+
companyId: string | null;
|
|
87
89
|
contactId: string | null;
|
|
88
90
|
createdAt: Date;
|
|
89
91
|
id: string;
|
|
90
92
|
name: string;
|
|
91
93
|
orgId: string;
|
|
92
94
|
owner: string | null;
|
|
95
|
+
/**
|
|
96
|
+
* The board this deal belongs to. NULL means the org's default pipeline: the
|
|
97
|
+
* sync mirror wrote deals before pipelines existed, and a deal created with
|
|
98
|
+
* no pipeline lands on the default. Stage keys are unique per pipeline, so
|
|
99
|
+
* a board must filter on this, never on stage key alone.
|
|
100
|
+
*/
|
|
101
|
+
pipelineId: string | null;
|
|
93
102
|
position: number | null;
|
|
94
103
|
stage: DealStage;
|
|
95
104
|
/**
|
|
@@ -116,6 +125,11 @@ export interface Task {
|
|
|
116
125
|
createdAt: Date;
|
|
117
126
|
createdBy: string | null;
|
|
118
127
|
description: string | null;
|
|
128
|
+
/**
|
|
129
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
130
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
131
|
+
*/
|
|
132
|
+
dueAllDay?: boolean | null;
|
|
119
133
|
dueAt: Date | null;
|
|
120
134
|
entityId: string | null;
|
|
121
135
|
entityType: TaskEntityType | null;
|
|
@@ -203,6 +217,11 @@ export interface CreateTaskInput {
|
|
|
203
217
|
assignee?: string;
|
|
204
218
|
createdBy?: string;
|
|
205
219
|
description?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
222
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
223
|
+
*/
|
|
224
|
+
dueAllDay?: boolean | null;
|
|
206
225
|
dueAt?: Date;
|
|
207
226
|
entityId?: string;
|
|
208
227
|
entityType?: TaskEntityType;
|
|
@@ -212,6 +231,11 @@ export interface CreateTaskInput {
|
|
|
212
231
|
export interface UpdateTaskInput {
|
|
213
232
|
assignee?: string | null;
|
|
214
233
|
description?: string | null;
|
|
234
|
+
/**
|
|
235
|
+
* Whether `dueAt` is a calendar date (stored as midnight UTC) rather than a
|
|
236
|
+
* moment. Absent or null: a midnight-UTC `dueAt` is read as a date.
|
|
237
|
+
*/
|
|
238
|
+
dueAllDay?: boolean | null;
|
|
215
239
|
dueAt?: Date | null;
|
|
216
240
|
entityId?: string | null;
|
|
217
241
|
entityType?: TaskEntityType | null;
|
|
@@ -244,6 +268,17 @@ export interface ContactFilters {
|
|
|
244
268
|
* row is no longer sync-provenanced.
|
|
245
269
|
*/
|
|
246
270
|
excludeArchived?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Case-insensitive substring match on name or email.
|
|
273
|
+
*
|
|
274
|
+
* Applied in SQL, not over an already-fetched page: a filter applied after
|
|
275
|
+
* paging returns a partial page and a cursor that skips the rows it dropped,
|
|
276
|
+
* so "page 2" of a search is not the second page of the matches. An empty or
|
|
277
|
+
* whitespace-only value is ignored rather than matching nothing.
|
|
278
|
+
*
|
|
279
|
+
* LIKE metacharacters in the term are escaped and matched literally — a
|
|
280
|
+
* search for `%` finds contacts containing a percent sign, not every row.
|
|
281
|
+
*/
|
|
247
282
|
search?: string;
|
|
248
283
|
source?: RecordSource;
|
|
249
284
|
status?: ContactStatus;
|