@clivly/sdk 0.4.0 → 0.7.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.
- package/README.md +6 -8
- package/dist/chat-session.cjs +9 -2
- package/dist/chat-session.d.cts +2 -8
- package/dist/chat-session.d.mts +2 -8
- package/dist/chat-session.mjs +9 -2
- package/dist/drizzle.cjs +8 -1
- package/dist/drizzle.d.cts +3 -3
- package/dist/drizzle.d.mts +3 -3
- package/dist/drizzle.mjs +3 -2
- package/dist/index.cjs +171 -32
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +166 -33
- package/dist/{types-B7wBCHRT.d.mts → types-DTaz5mLi.d.cts} +20 -6
- package/dist/{types-jMafEyTv.d.cts → types-DTaz5mLi.d.mts} +20 -6
- package/dist/vite.d.cts +1 -2
- package/dist/vite.d.mts +1 -2
- package/package.json +7 -4
- /package/dist/{drizzle-meta-iAu8w1hj.cjs → drizzle-meta-CdCs30q3.cjs} +0 -0
- /package/dist/{drizzle-meta-DMx_9nlj.mjs → drizzle-meta-DNqzkREf.mjs} +0 -0
package/README.md
CHANGED
|
@@ -18,15 +18,14 @@ npm install clivly
|
|
|
18
18
|
Clivly's stable packages publish on npm's `latest` tag. Use `@next` only when
|
|
19
19
|
you are intentionally validating a prerelease build.
|
|
20
20
|
|
|
21
|
-
Add `drizzle-orm` when you use `clivly/drizzle
|
|
21
|
+
Add `drizzle-orm` when you use `clivly/drizzle`, and
|
|
22
22
|
`vite` when you use the Vite/TanStack Start plugin.
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import { createClivlySDK } from "clivly/sdk";
|
|
27
|
+
import { createClivlySDK, defineClivlyConfig } from "clivly/sdk";
|
|
28
28
|
import { fromDrizzle } from "clivly/drizzle";
|
|
29
|
-
import { defineClivlyConfig } from "clivly/core";
|
|
30
29
|
import { db, participants, accounts } from "./db";
|
|
31
30
|
|
|
32
31
|
// Declarative entity config — the non-deprecated source of truth. It maps your
|
|
@@ -48,7 +47,7 @@ const entities = defineClivlyConfig({
|
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
const clivly = createClivlySDK({
|
|
51
|
-
apiKey: process.env.
|
|
50
|
+
apiKey: process.env.CLIVLY_SECRET_KEY!, // sk_live_… from Settings → API keys
|
|
52
51
|
entities,
|
|
53
52
|
source: {
|
|
54
53
|
contacts: fromDrizzle(db, participants, { entity: "participants", cursorField: "updatedAt" }),
|
|
@@ -63,7 +62,7 @@ await clivly.start(); // heartbeat + backfill + interval delta sync
|
|
|
63
62
|
|
|
64
63
|
// Optional: let Clivly's dashboard start a sync in this app.
|
|
65
64
|
const remote = createClivlySDK({
|
|
66
|
-
apiKey: process.env.
|
|
65
|
+
apiKey: process.env.CLIVLY_SECRET_KEY!,
|
|
67
66
|
entities,
|
|
68
67
|
source: { /* … */ },
|
|
69
68
|
syncTrigger: {
|
|
@@ -71,7 +70,6 @@ const remote = createClivlySDK({
|
|
|
71
70
|
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
|
|
72
71
|
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET, // whsec_… from Integrations → Remote sync
|
|
73
72
|
},
|
|
74
|
-
verifyToken: process.env.CLIVLY_VERIFY_TOKEN,
|
|
75
73
|
});
|
|
76
74
|
|
|
77
75
|
export async function POST(request: Request) {
|
|
@@ -126,7 +124,7 @@ the heartbeat and sync loop. It is dev-server only; use `runScheduled()` or
|
|
|
126
124
|
Put your instance in `clivly.config.ts` (`export default createClivlySDK({...})`), then:
|
|
127
125
|
|
|
128
126
|
```bash
|
|
129
|
-
npx clivly ping #
|
|
127
|
+
npx clivly ping # CLIVLY_SECRET_KEY + API reachability
|
|
130
128
|
npx clivly doctor # env → config → schema → columns → sample fetch → heartbeat
|
|
131
129
|
```
|
|
132
130
|
|
|
@@ -170,7 +168,7 @@ serverless cold start**, so a fresh isolate would re-page the whole entity. Pass
|
|
|
170
168
|
|
|
171
169
|
```ts
|
|
172
170
|
const clivly = createClivlySDK({
|
|
173
|
-
apiKey: process.env.
|
|
171
|
+
apiKey: process.env.CLIVLY_SECRET_KEY!,
|
|
174
172
|
entities,
|
|
175
173
|
source: { /* … */ },
|
|
176
174
|
cursorStore: {
|
package/dist/chat-session.cjs
CHANGED
|
@@ -36,7 +36,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
36
36
|
if (!origin) return false;
|
|
37
37
|
return allowedOrigins.includes(origin);
|
|
38
38
|
}
|
|
39
|
-
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch }) {
|
|
39
|
+
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch, resolveUser }) {
|
|
40
40
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
41
41
|
return async (request) => {
|
|
42
42
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
@@ -55,6 +55,12 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
55
55
|
return jsonResponse({ error: "invalid_json" }, 400);
|
|
56
56
|
}
|
|
57
57
|
if (!isChatSessionRequestBody(body)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
58
|
+
let hostUser = null;
|
|
59
|
+
if (resolveUser) try {
|
|
60
|
+
hostUser = await resolveUser(request);
|
|
61
|
+
} catch {
|
|
62
|
+
console.warn("[clivly] resolveUser threw; continuing without host identity");
|
|
63
|
+
}
|
|
58
64
|
try {
|
|
59
65
|
const upstream = await fetchImpl(sessionUrl, {
|
|
60
66
|
method: "POST",
|
|
@@ -67,7 +73,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
67
73
|
widgetId: body.widgetId,
|
|
68
74
|
visitor: body.visitor,
|
|
69
75
|
visitorToken: body.visitorToken,
|
|
70
|
-
origin
|
|
76
|
+
origin,
|
|
77
|
+
...hostUser ? { hostUser } : {}
|
|
71
78
|
})
|
|
72
79
|
});
|
|
73
80
|
const responseBody = await upstream.text();
|
package/dist/chat-session.d.cts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { c as CreateChatSessionHandlerOptions } from "./types-
|
|
2
|
-
|
|
1
|
+
import { c as CreateChatSessionHandlerOptions } from "./types-DTaz5mLi.cjs";
|
|
3
2
|
//#region src/chat-session.d.ts
|
|
4
|
-
declare function createChatSessionHandler({
|
|
5
|
-
allowedOrigins,
|
|
6
|
-
apiKey,
|
|
7
|
-
apiUrl,
|
|
8
|
-
fetchImpl
|
|
9
|
-
}: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
|
|
3
|
+
declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
|
|
10
4
|
//#endregion
|
|
11
5
|
export { createChatSessionHandler };
|
package/dist/chat-session.d.mts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { c as CreateChatSessionHandlerOptions } from "./types-
|
|
2
|
-
|
|
1
|
+
import { c as CreateChatSessionHandlerOptions } from "./types-DTaz5mLi.mjs";
|
|
3
2
|
//#region src/chat-session.d.ts
|
|
4
|
-
declare function createChatSessionHandler({
|
|
5
|
-
allowedOrigins,
|
|
6
|
-
apiKey,
|
|
7
|
-
apiUrl,
|
|
8
|
-
fetchImpl
|
|
9
|
-
}: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
|
|
3
|
+
declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
|
|
10
4
|
//#endregion
|
|
11
5
|
export { createChatSessionHandler };
|
package/dist/chat-session.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function originAllowed(origin, allowedOrigins) {
|
|
|
35
35
|
if (!origin) return false;
|
|
36
36
|
return allowedOrigins.includes(origin);
|
|
37
37
|
}
|
|
38
|
-
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch }) {
|
|
38
|
+
function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API_URL, fetchImpl = fetch, resolveUser }) {
|
|
39
39
|
const sessionUrl = `${normalizeApiUrl(apiUrl)}/clivly/widget/session`;
|
|
40
40
|
return async (request) => {
|
|
41
41
|
if (request.method !== "POST") return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
@@ -54,6 +54,12 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
54
54
|
return jsonResponse({ error: "invalid_json" }, 400);
|
|
55
55
|
}
|
|
56
56
|
if (!isChatSessionRequestBody(body)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
57
|
+
let hostUser = null;
|
|
58
|
+
if (resolveUser) try {
|
|
59
|
+
hostUser = await resolveUser(request);
|
|
60
|
+
} catch {
|
|
61
|
+
console.warn("[clivly] resolveUser threw; continuing without host identity");
|
|
62
|
+
}
|
|
57
63
|
try {
|
|
58
64
|
const upstream = await fetchImpl(sessionUrl, {
|
|
59
65
|
method: "POST",
|
|
@@ -66,7 +72,8 @@ function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl = DEFAULT_API
|
|
|
66
72
|
widgetId: body.widgetId,
|
|
67
73
|
visitor: body.visitor,
|
|
68
74
|
visitorToken: body.visitorToken,
|
|
69
|
-
origin
|
|
75
|
+
origin,
|
|
76
|
+
...hostUser ? { hostUser } : {}
|
|
70
77
|
})
|
|
71
78
|
});
|
|
72
79
|
const responseBody = await upstream.text();
|
package/dist/drizzle.cjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_drizzle_meta = require("./drizzle-meta-
|
|
2
|
+
const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
|
|
3
3
|
let drizzle_orm = require("drizzle-orm");
|
|
4
4
|
let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
|
|
5
|
+
let _clivly_core_drizzle = require("@clivly/core/drizzle");
|
|
5
6
|
//#region src/drizzle.ts
|
|
6
7
|
function buildColumnsMeta(table) {
|
|
7
8
|
const columns = (0, drizzle_orm.getTableColumns)(table);
|
|
@@ -103,5 +104,11 @@ function drizzleIntrospector(db) {
|
|
|
103
104
|
};
|
|
104
105
|
}
|
|
105
106
|
//#endregion
|
|
107
|
+
Object.defineProperty(exports, "discoverFromDrizzle", {
|
|
108
|
+
enumerable: true,
|
|
109
|
+
get: function() {
|
|
110
|
+
return _clivly_core_drizzle.discoverFromDrizzle;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
106
113
|
exports.drizzleIntrospector = drizzleIntrospector;
|
|
107
114
|
exports.fromDrizzle = fromDrizzle;
|
package/dist/drizzle.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { m as NamespaceIntrospector, p as SyncSource } from "./types-
|
|
1
|
+
import { m as NamespaceIntrospector, p as SyncSource } from "./types-DTaz5mLi.cjs";
|
|
2
2
|
import { Table } from "drizzle-orm";
|
|
3
|
-
|
|
3
|
+
import { discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
4
4
|
//#region src/drizzle.d.ts
|
|
5
5
|
interface DrizzleDb {
|
|
6
6
|
select: (...args: any[]) => any;
|
|
@@ -25,4 +25,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
|
25
25
|
*/
|
|
26
26
|
declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
|
|
27
27
|
//#endregion
|
|
28
|
-
export { drizzleIntrospector, fromDrizzle };
|
|
28
|
+
export { discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
|
package/dist/drizzle.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { m as NamespaceIntrospector, p as SyncSource } from "./types-
|
|
1
|
+
import { m as NamespaceIntrospector, p as SyncSource } from "./types-DTaz5mLi.mjs";
|
|
2
2
|
import { Table } from "drizzle-orm";
|
|
3
|
-
|
|
3
|
+
import { discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
4
4
|
//#region src/drizzle.d.ts
|
|
5
5
|
interface DrizzleDb {
|
|
6
6
|
select: (...args: any[]) => any;
|
|
@@ -25,4 +25,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
|
25
25
|
*/
|
|
26
26
|
declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
|
|
27
27
|
//#endregion
|
|
28
|
-
export { drizzleIntrospector, fromDrizzle };
|
|
28
|
+
export { discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
|
package/dist/drizzle.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { t as DRIZZLE_META } from "./drizzle-meta-
|
|
1
|
+
import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
|
|
2
2
|
import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
|
|
3
3
|
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
4
|
+
import { discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
4
5
|
//#region src/drizzle.ts
|
|
5
6
|
function buildColumnsMeta(table) {
|
|
6
7
|
const columns = getTableColumns(table);
|
|
@@ -102,4 +103,4 @@ function drizzleIntrospector(db) {
|
|
|
102
103
|
};
|
|
103
104
|
}
|
|
104
105
|
//#endregion
|
|
105
|
-
export { drizzleIntrospector, fromDrizzle };
|
|
106
|
+
export { discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_drizzle_meta = require("./drizzle-meta-
|
|
2
|
+
const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
|
|
3
3
|
const require_chat_session = require("./chat-session.cjs");
|
|
4
|
+
let _clivly_core_entity_config = require("@clivly/core/entity-config");
|
|
5
|
+
//#region src/entity-source.ts
|
|
6
|
+
const CONCEPT_KEYS = {
|
|
7
|
+
contact: "contacts",
|
|
8
|
+
company: "companies",
|
|
9
|
+
deal: "deals"
|
|
10
|
+
};
|
|
11
|
+
function resolveEntitySource(config, entityKey) {
|
|
12
|
+
const entity = config.entities?.entities?.[entityKey];
|
|
13
|
+
const src = config.source;
|
|
14
|
+
if (!(entity && src)) return null;
|
|
15
|
+
const conceptKey = CONCEPT_KEYS[entity.concept];
|
|
16
|
+
if (conceptKey) {
|
|
17
|
+
const source = src[conceptKey];
|
|
18
|
+
return source ? {
|
|
19
|
+
concept: entity.concept,
|
|
20
|
+
expectedKey: conceptKey,
|
|
21
|
+
source
|
|
22
|
+
} : null;
|
|
23
|
+
}
|
|
24
|
+
if (entity.concept === "custom") {
|
|
25
|
+
const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
26
|
+
return match ? {
|
|
27
|
+
concept: "custom",
|
|
28
|
+
expectedKey: "custom",
|
|
29
|
+
source: match
|
|
30
|
+
} : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The `config.source` key an entity's concept requires, present or not. */
|
|
35
|
+
function expectedSourceKey(concept) {
|
|
36
|
+
return CONCEPT_KEYS[concept] ?? "custom";
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/concept-coverage.ts
|
|
40
|
+
const CHECK_NAME = "Entity → source coverage";
|
|
41
|
+
function configuredKeys(config) {
|
|
42
|
+
const src = config.source;
|
|
43
|
+
if (!src) return [];
|
|
44
|
+
return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every configured entity must resolve to a source, or it silently syncs
|
|
48
|
+
* nothing. This is the check that catches `source` keyed by entity name rather
|
|
49
|
+
* than by CRM concept — a mistake that otherwise shows up only as a run that
|
|
50
|
+
* succeeds with zero rows.
|
|
51
|
+
*/
|
|
52
|
+
function checkConceptCoverage(config) {
|
|
53
|
+
const entities = config.entities?.entities ?? {};
|
|
54
|
+
const keys = Object.keys(entities);
|
|
55
|
+
if (keys.length === 0) return {
|
|
56
|
+
name: CHECK_NAME,
|
|
57
|
+
ok: false,
|
|
58
|
+
skipped: true,
|
|
59
|
+
detail: "No entities configured — nothing to cross-check."
|
|
60
|
+
};
|
|
61
|
+
const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
|
|
62
|
+
if (unresolved.length === 0) return {
|
|
63
|
+
name: CHECK_NAME,
|
|
64
|
+
ok: true,
|
|
65
|
+
detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
|
|
66
|
+
};
|
|
67
|
+
const available = configuredKeys(config);
|
|
68
|
+
const listed = available.length > 0 ? available.join(", ") : "(none)";
|
|
69
|
+
const problems = unresolved.map((key) => {
|
|
70
|
+
const concept = entities[key]?.concept ?? "unknown";
|
|
71
|
+
if (concept === "custom") {
|
|
72
|
+
const custom = config.source?.custom ?? [];
|
|
73
|
+
if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
|
|
74
|
+
}
|
|
75
|
+
return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
name: CHECK_NAME,
|
|
79
|
+
ok: false,
|
|
80
|
+
detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
4
84
|
//#region src/custom-slug.ts
|
|
5
85
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
6
86
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
@@ -81,6 +161,56 @@ function describeFetchError(entity, error) {
|
|
|
81
161
|
return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
|
|
82
162
|
}
|
|
83
163
|
//#endregion
|
|
164
|
+
//#region src/mapping-dry-run.ts
|
|
165
|
+
/**
|
|
166
|
+
* Fetch one real row and push it through the entity's field map.
|
|
167
|
+
*
|
|
168
|
+
* The distinction from "Sample fetch": that proves the query runs, this proves
|
|
169
|
+
* the row that comes back actually carries the columns `fields` names. A
|
|
170
|
+
* mapping that points at a column the row lacks produces empty CRM records
|
|
171
|
+
* rather than an error, so it has to be caught here.
|
|
172
|
+
*
|
|
173
|
+
* An empty table SKIPS rather than fails — a fresh database is not a
|
|
174
|
+
* misconfiguration, and failing on it would train people to ignore the check.
|
|
175
|
+
*/
|
|
176
|
+
async function runMappingDryRun(config, entityKey) {
|
|
177
|
+
const name = `Field mapping dry run (${entityKey})`;
|
|
178
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
179
|
+
if (!resolved) return {
|
|
180
|
+
name,
|
|
181
|
+
ok: false,
|
|
182
|
+
skipped: true,
|
|
183
|
+
detail: `"${entityKey}" resolves to no source — see the coverage check above.`
|
|
184
|
+
};
|
|
185
|
+
const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
|
|
186
|
+
let rows;
|
|
187
|
+
try {
|
|
188
|
+
rows = await resolved.source.fetchPage({
|
|
189
|
+
since: null,
|
|
190
|
+
limit: 1
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return {
|
|
194
|
+
name,
|
|
195
|
+
ok: false,
|
|
196
|
+
detail: describeFetchError(entityKey, error)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const row = rows[0];
|
|
200
|
+
if (row === void 0) return {
|
|
201
|
+
name,
|
|
202
|
+
ok: false,
|
|
203
|
+
skipped: true,
|
|
204
|
+
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
205
|
+
};
|
|
206
|
+
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
207
|
+
return {
|
|
208
|
+
name,
|
|
209
|
+
ok: missing.length === 0,
|
|
210
|
+
detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
84
214
|
//#region src/namespace-probe.ts
|
|
85
215
|
/**
|
|
86
216
|
* Verify the tables the integration depends on are genuinely reachable:
|
|
@@ -171,6 +301,11 @@ async function namespaceCheck(config) {
|
|
|
171
301
|
detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
|
|
172
302
|
};
|
|
173
303
|
}
|
|
304
|
+
async function mappingDryRunChecks(config) {
|
|
305
|
+
const checks = [];
|
|
306
|
+
for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
|
|
307
|
+
return checks;
|
|
308
|
+
}
|
|
174
309
|
/**
|
|
175
310
|
* Run the read-only setup checks and return a structured report. Every check
|
|
176
311
|
* captures its own error into `detail` rather than throwing, so one failure never
|
|
@@ -197,6 +332,7 @@ async function runDoctor(args) {
|
|
|
197
332
|
ok: sources.length > 0,
|
|
198
333
|
detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
|
|
199
334
|
});
|
|
335
|
+
checks.push(checkConceptCoverage(config));
|
|
200
336
|
for (const source of sources) {
|
|
201
337
|
const meta = source[require_drizzle_meta.DRIZZLE_META];
|
|
202
338
|
if (!meta) {
|
|
@@ -235,6 +371,7 @@ async function runDoctor(args) {
|
|
|
235
371
|
detail: describeFetchError(source.entity, error)
|
|
236
372
|
});
|
|
237
373
|
}
|
|
374
|
+
checks.push(...await mappingDryRunChecks(config));
|
|
238
375
|
const result = await connect();
|
|
239
376
|
checks.push({
|
|
240
377
|
name: "Heartbeat",
|
|
@@ -278,6 +415,7 @@ async function syncSource(args) {
|
|
|
278
415
|
const idField = source.idField ?? DEFAULT_ID_FIELD;
|
|
279
416
|
let cursor = since;
|
|
280
417
|
let pushed = 0;
|
|
418
|
+
const resumedFrom = since === null ? null : since.time.toISOString();
|
|
281
419
|
for (;;) {
|
|
282
420
|
const rows = await source.fetchPage({
|
|
283
421
|
since: cursor,
|
|
@@ -289,6 +427,8 @@ async function syncSource(args) {
|
|
|
289
427
|
source: source.entity,
|
|
290
428
|
rows: capped,
|
|
291
429
|
mode: "delta",
|
|
430
|
+
fetched: rows.length,
|
|
431
|
+
cursor: resumedFrom,
|
|
292
432
|
...objectType ? { objectType } : {}
|
|
293
433
|
})) return {
|
|
294
434
|
pushed,
|
|
@@ -331,6 +471,8 @@ async function fullSyncSource(args) {
|
|
|
331
471
|
source: source.entity,
|
|
332
472
|
rows: all,
|
|
333
473
|
mode: "full",
|
|
474
|
+
fetched: all.length,
|
|
475
|
+
cursor: null,
|
|
334
476
|
...objectType ? { objectType } : {}
|
|
335
477
|
});
|
|
336
478
|
return {
|
|
@@ -395,9 +537,7 @@ function observedTriggerUrl(req) {
|
|
|
395
537
|
const proto = forwardedProto || requestUrl.protocol.replace(":", "");
|
|
396
538
|
try {
|
|
397
539
|
return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
|
|
398
|
-
} catch {
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
540
|
+
} catch {}
|
|
401
541
|
}
|
|
402
542
|
/**
|
|
403
543
|
* Decide which trigger URL to report, in precedence order:
|
|
@@ -595,9 +735,7 @@ function connectReason(status) {
|
|
|
595
735
|
async function resolveOrg(response) {
|
|
596
736
|
try {
|
|
597
737
|
return (await response.json())?.org;
|
|
598
|
-
} catch {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
738
|
+
} catch {}
|
|
601
739
|
}
|
|
602
740
|
function retryAfterMs(res) {
|
|
603
741
|
const header = res.headers.get("retry-after");
|
|
@@ -620,35 +758,30 @@ function createFetcher(retry) {
|
|
|
620
758
|
}
|
|
621
759
|
if (!(res === null || isRetryableStatus(res.status)) || attempt >= maxAttempts) return res;
|
|
622
760
|
const backoff = baseDelayMs * 2 ** (attempt - 1) + Math.random() * baseDelayMs;
|
|
623
|
-
|
|
761
|
+
const wait = res ? retryAfterMs(res) ?? backoff : backoff;
|
|
762
|
+
await sleep(wait);
|
|
624
763
|
}
|
|
625
764
|
};
|
|
626
765
|
}
|
|
766
|
+
const CONCEPT_PATHS = {
|
|
767
|
+
contacts: SYNC_CONTACTS_PATH,
|
|
768
|
+
companies: SYNC_COMPANIES_PATH,
|
|
769
|
+
deals: SYNC_DEALS_PATH,
|
|
770
|
+
custom: SYNC_CUSTOM_PATH
|
|
771
|
+
};
|
|
627
772
|
function resolveSampleSource(config, entityKey) {
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
773
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
774
|
+
if (!resolved) return null;
|
|
775
|
+
const path = CONCEPT_PATHS[resolved.expectedKey];
|
|
776
|
+
if (!path) return null;
|
|
777
|
+
return resolved.expectedKey === "custom" ? {
|
|
778
|
+
source: resolved.source,
|
|
779
|
+
path,
|
|
780
|
+
objectType: resolved.source.objectType
|
|
781
|
+
} : {
|
|
782
|
+
source: resolved.source,
|
|
783
|
+
path
|
|
638
784
|
};
|
|
639
|
-
if (entity.concept === "deal" && src.deals) return {
|
|
640
|
-
source: src.deals,
|
|
641
|
-
path: SYNC_DEALS_PATH
|
|
642
|
-
};
|
|
643
|
-
if (entity.concept === "custom") {
|
|
644
|
-
const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
645
|
-
return custom ? {
|
|
646
|
-
source: custom,
|
|
647
|
-
path: SYNC_CUSTOM_PATH,
|
|
648
|
-
objectType: custom.objectType
|
|
649
|
-
} : null;
|
|
650
|
-
}
|
|
651
|
-
return null;
|
|
652
785
|
}
|
|
653
786
|
const PREVIEW_LIMIT = 5;
|
|
654
787
|
function resolvePreviewSource(config, sourceTable) {
|
|
@@ -677,7 +810,7 @@ async function handlePreviewRequest(config, preview) {
|
|
|
677
810
|
if (!source) return jsonResponse({ error: `No configured source for table "${preview.sourceTable}"` }, 400);
|
|
678
811
|
const idField = source.idField ?? "id";
|
|
679
812
|
const filterColumns = preview.filter ? Object.keys(preview.filter).filter((k) => k !== "$raw") : [];
|
|
680
|
-
const needed = new Set([
|
|
813
|
+
const needed = /* @__PURE__ */ new Set([
|
|
681
814
|
idField,
|
|
682
815
|
...Object.values(preview.fieldMap),
|
|
683
816
|
...filterColumns
|
|
@@ -939,3 +1072,9 @@ exports.ClivlyNetworkError = ClivlyNetworkError;
|
|
|
939
1072
|
exports.ClivlyRateLimitError = ClivlyRateLimitError;
|
|
940
1073
|
exports.createChatSessionHandler = require_chat_session.createChatSessionHandler;
|
|
941
1074
|
exports.createClivlySDK = createClivlySDK;
|
|
1075
|
+
Object.defineProperty(exports, "defineClivlyConfig", {
|
|
1076
|
+
enumerable: true,
|
|
1077
|
+
get: function() {
|
|
1078
|
+
return _clivly_core_entity_config.defineClivlyConfig;
|
|
1079
|
+
}
|
|
1080
|
+
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-
|
|
1
|
+
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-DTaz5mLi.cjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.cjs";
|
|
3
|
-
|
|
3
|
+
import { ClivlyEntitiesConfig, defineClivlyConfig } from "@clivly/core/entity-config";
|
|
4
4
|
//#region src/errors.d.ts
|
|
5
5
|
declare class ClivlyError extends Error {
|
|
6
6
|
/** HTTP status, or null when the request never completed (network error). */
|
|
@@ -31,4 +31,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
|
|
|
31
31
|
*/
|
|
32
32
|
declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
|
|
33
33
|
//#endregion
|
|
34
|
-
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK };
|
|
34
|
+
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-
|
|
1
|
+
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, i as ClivlyConnectResult, l as CursorStore, n as ChatWidgetVisitor, o as ClivlySDKConfig, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-DTaz5mLi.mjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
|
-
|
|
3
|
+
import { ClivlyEntitiesConfig, defineClivlyConfig } from "@clivly/core/entity-config";
|
|
4
4
|
//#region src/errors.d.ts
|
|
5
5
|
declare class ClivlyError extends Error {
|
|
6
6
|
/** HTTP status, or null when the request never completed (network error). */
|
|
@@ -31,4 +31,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
|
|
|
31
31
|
*/
|
|
32
32
|
declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
|
|
33
33
|
//#endregion
|
|
34
|
-
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK };
|
|
34
|
+
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type CreateChatSessionHandlerOptions, type CursorStore, type DiscoveredTable, type DoctorCheck, type DoctorReport, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
|
-
import { t as DRIZZLE_META } from "./drizzle-meta-
|
|
1
|
+
import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
|
+
import { defineClivlyConfig } from "@clivly/core/entity-config";
|
|
4
|
+
//#region src/entity-source.ts
|
|
5
|
+
const CONCEPT_KEYS = {
|
|
6
|
+
contact: "contacts",
|
|
7
|
+
company: "companies",
|
|
8
|
+
deal: "deals"
|
|
9
|
+
};
|
|
10
|
+
function resolveEntitySource(config, entityKey) {
|
|
11
|
+
const entity = config.entities?.entities?.[entityKey];
|
|
12
|
+
const src = config.source;
|
|
13
|
+
if (!(entity && src)) return null;
|
|
14
|
+
const conceptKey = CONCEPT_KEYS[entity.concept];
|
|
15
|
+
if (conceptKey) {
|
|
16
|
+
const source = src[conceptKey];
|
|
17
|
+
return source ? {
|
|
18
|
+
concept: entity.concept,
|
|
19
|
+
expectedKey: conceptKey,
|
|
20
|
+
source
|
|
21
|
+
} : null;
|
|
22
|
+
}
|
|
23
|
+
if (entity.concept === "custom") {
|
|
24
|
+
const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
25
|
+
return match ? {
|
|
26
|
+
concept: "custom",
|
|
27
|
+
expectedKey: "custom",
|
|
28
|
+
source: match
|
|
29
|
+
} : null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/** The `config.source` key an entity's concept requires, present or not. */
|
|
34
|
+
function expectedSourceKey(concept) {
|
|
35
|
+
return CONCEPT_KEYS[concept] ?? "custom";
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/concept-coverage.ts
|
|
39
|
+
const CHECK_NAME = "Entity → source coverage";
|
|
40
|
+
function configuredKeys(config) {
|
|
41
|
+
const src = config.source;
|
|
42
|
+
if (!src) return [];
|
|
43
|
+
return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Every configured entity must resolve to a source, or it silently syncs
|
|
47
|
+
* nothing. This is the check that catches `source` keyed by entity name rather
|
|
48
|
+
* than by CRM concept — a mistake that otherwise shows up only as a run that
|
|
49
|
+
* succeeds with zero rows.
|
|
50
|
+
*/
|
|
51
|
+
function checkConceptCoverage(config) {
|
|
52
|
+
const entities = config.entities?.entities ?? {};
|
|
53
|
+
const keys = Object.keys(entities);
|
|
54
|
+
if (keys.length === 0) return {
|
|
55
|
+
name: CHECK_NAME,
|
|
56
|
+
ok: false,
|
|
57
|
+
skipped: true,
|
|
58
|
+
detail: "No entities configured — nothing to cross-check."
|
|
59
|
+
};
|
|
60
|
+
const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
|
|
61
|
+
if (unresolved.length === 0) return {
|
|
62
|
+
name: CHECK_NAME,
|
|
63
|
+
ok: true,
|
|
64
|
+
detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
|
|
65
|
+
};
|
|
66
|
+
const available = configuredKeys(config);
|
|
67
|
+
const listed = available.length > 0 ? available.join(", ") : "(none)";
|
|
68
|
+
const problems = unresolved.map((key) => {
|
|
69
|
+
const concept = entities[key]?.concept ?? "unknown";
|
|
70
|
+
if (concept === "custom") {
|
|
71
|
+
const custom = config.source?.custom ?? [];
|
|
72
|
+
if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
|
|
73
|
+
}
|
|
74
|
+
return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
name: CHECK_NAME,
|
|
78
|
+
ok: false,
|
|
79
|
+
detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
3
83
|
//#region src/custom-slug.ts
|
|
4
84
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
5
85
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
@@ -80,6 +160,56 @@ function describeFetchError(entity, error) {
|
|
|
80
160
|
return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
|
|
81
161
|
}
|
|
82
162
|
//#endregion
|
|
163
|
+
//#region src/mapping-dry-run.ts
|
|
164
|
+
/**
|
|
165
|
+
* Fetch one real row and push it through the entity's field map.
|
|
166
|
+
*
|
|
167
|
+
* The distinction from "Sample fetch": that proves the query runs, this proves
|
|
168
|
+
* the row that comes back actually carries the columns `fields` names. A
|
|
169
|
+
* mapping that points at a column the row lacks produces empty CRM records
|
|
170
|
+
* rather than an error, so it has to be caught here.
|
|
171
|
+
*
|
|
172
|
+
* An empty table SKIPS rather than fails — a fresh database is not a
|
|
173
|
+
* misconfiguration, and failing on it would train people to ignore the check.
|
|
174
|
+
*/
|
|
175
|
+
async function runMappingDryRun(config, entityKey) {
|
|
176
|
+
const name = `Field mapping dry run (${entityKey})`;
|
|
177
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
178
|
+
if (!resolved) return {
|
|
179
|
+
name,
|
|
180
|
+
ok: false,
|
|
181
|
+
skipped: true,
|
|
182
|
+
detail: `"${entityKey}" resolves to no source — see the coverage check above.`
|
|
183
|
+
};
|
|
184
|
+
const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
|
|
185
|
+
let rows;
|
|
186
|
+
try {
|
|
187
|
+
rows = await resolved.source.fetchPage({
|
|
188
|
+
since: null,
|
|
189
|
+
limit: 1
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return {
|
|
193
|
+
name,
|
|
194
|
+
ok: false,
|
|
195
|
+
detail: describeFetchError(entityKey, error)
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const row = rows[0];
|
|
199
|
+
if (row === void 0) return {
|
|
200
|
+
name,
|
|
201
|
+
ok: false,
|
|
202
|
+
skipped: true,
|
|
203
|
+
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
204
|
+
};
|
|
205
|
+
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
206
|
+
return {
|
|
207
|
+
name,
|
|
208
|
+
ok: missing.length === 0,
|
|
209
|
+
detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
83
213
|
//#region src/namespace-probe.ts
|
|
84
214
|
/**
|
|
85
215
|
* Verify the tables the integration depends on are genuinely reachable:
|
|
@@ -170,6 +300,11 @@ async function namespaceCheck(config) {
|
|
|
170
300
|
detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
|
|
171
301
|
};
|
|
172
302
|
}
|
|
303
|
+
async function mappingDryRunChecks(config) {
|
|
304
|
+
const checks = [];
|
|
305
|
+
for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
|
|
306
|
+
return checks;
|
|
307
|
+
}
|
|
173
308
|
/**
|
|
174
309
|
* Run the read-only setup checks and return a structured report. Every check
|
|
175
310
|
* captures its own error into `detail` rather than throwing, so one failure never
|
|
@@ -196,6 +331,7 @@ async function runDoctor(args) {
|
|
|
196
331
|
ok: sources.length > 0,
|
|
197
332
|
detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
|
|
198
333
|
});
|
|
334
|
+
checks.push(checkConceptCoverage(config));
|
|
199
335
|
for (const source of sources) {
|
|
200
336
|
const meta = source[DRIZZLE_META];
|
|
201
337
|
if (!meta) {
|
|
@@ -234,6 +370,7 @@ async function runDoctor(args) {
|
|
|
234
370
|
detail: describeFetchError(source.entity, error)
|
|
235
371
|
});
|
|
236
372
|
}
|
|
373
|
+
checks.push(...await mappingDryRunChecks(config));
|
|
237
374
|
const result = await connect();
|
|
238
375
|
checks.push({
|
|
239
376
|
name: "Heartbeat",
|
|
@@ -277,6 +414,7 @@ async function syncSource(args) {
|
|
|
277
414
|
const idField = source.idField ?? DEFAULT_ID_FIELD;
|
|
278
415
|
let cursor = since;
|
|
279
416
|
let pushed = 0;
|
|
417
|
+
const resumedFrom = since === null ? null : since.time.toISOString();
|
|
280
418
|
for (;;) {
|
|
281
419
|
const rows = await source.fetchPage({
|
|
282
420
|
since: cursor,
|
|
@@ -288,6 +426,8 @@ async function syncSource(args) {
|
|
|
288
426
|
source: source.entity,
|
|
289
427
|
rows: capped,
|
|
290
428
|
mode: "delta",
|
|
429
|
+
fetched: rows.length,
|
|
430
|
+
cursor: resumedFrom,
|
|
291
431
|
...objectType ? { objectType } : {}
|
|
292
432
|
})) return {
|
|
293
433
|
pushed,
|
|
@@ -330,6 +470,8 @@ async function fullSyncSource(args) {
|
|
|
330
470
|
source: source.entity,
|
|
331
471
|
rows: all,
|
|
332
472
|
mode: "full",
|
|
473
|
+
fetched: all.length,
|
|
474
|
+
cursor: null,
|
|
333
475
|
...objectType ? { objectType } : {}
|
|
334
476
|
});
|
|
335
477
|
return {
|
|
@@ -394,9 +536,7 @@ function observedTriggerUrl(req) {
|
|
|
394
536
|
const proto = forwardedProto || requestUrl.protocol.replace(":", "");
|
|
395
537
|
try {
|
|
396
538
|
return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
|
|
397
|
-
} catch {
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
539
|
+
} catch {}
|
|
400
540
|
}
|
|
401
541
|
/**
|
|
402
542
|
* Decide which trigger URL to report, in precedence order:
|
|
@@ -594,9 +734,7 @@ function connectReason(status) {
|
|
|
594
734
|
async function resolveOrg(response) {
|
|
595
735
|
try {
|
|
596
736
|
return (await response.json())?.org;
|
|
597
|
-
} catch {
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
737
|
+
} catch {}
|
|
600
738
|
}
|
|
601
739
|
function retryAfterMs(res) {
|
|
602
740
|
const header = res.headers.get("retry-after");
|
|
@@ -619,35 +757,30 @@ function createFetcher(retry) {
|
|
|
619
757
|
}
|
|
620
758
|
if (!(res === null || isRetryableStatus(res.status)) || attempt >= maxAttempts) return res;
|
|
621
759
|
const backoff = baseDelayMs * 2 ** (attempt - 1) + Math.random() * baseDelayMs;
|
|
622
|
-
|
|
760
|
+
const wait = res ? retryAfterMs(res) ?? backoff : backoff;
|
|
761
|
+
await sleep(wait);
|
|
623
762
|
}
|
|
624
763
|
};
|
|
625
764
|
}
|
|
765
|
+
const CONCEPT_PATHS = {
|
|
766
|
+
contacts: SYNC_CONTACTS_PATH,
|
|
767
|
+
companies: SYNC_COMPANIES_PATH,
|
|
768
|
+
deals: SYNC_DEALS_PATH,
|
|
769
|
+
custom: SYNC_CUSTOM_PATH
|
|
770
|
+
};
|
|
626
771
|
function resolveSampleSource(config, entityKey) {
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (entity.concept === "deal" && src.deals) return {
|
|
639
|
-
source: src.deals,
|
|
640
|
-
path: SYNC_DEALS_PATH
|
|
772
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
773
|
+
if (!resolved) return null;
|
|
774
|
+
const path = CONCEPT_PATHS[resolved.expectedKey];
|
|
775
|
+
if (!path) return null;
|
|
776
|
+
return resolved.expectedKey === "custom" ? {
|
|
777
|
+
source: resolved.source,
|
|
778
|
+
path,
|
|
779
|
+
objectType: resolved.source.objectType
|
|
780
|
+
} : {
|
|
781
|
+
source: resolved.source,
|
|
782
|
+
path
|
|
641
783
|
};
|
|
642
|
-
if (entity.concept === "custom") {
|
|
643
|
-
const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
644
|
-
return custom ? {
|
|
645
|
-
source: custom,
|
|
646
|
-
path: SYNC_CUSTOM_PATH,
|
|
647
|
-
objectType: custom.objectType
|
|
648
|
-
} : null;
|
|
649
|
-
}
|
|
650
|
-
return null;
|
|
651
784
|
}
|
|
652
785
|
const PREVIEW_LIMIT = 5;
|
|
653
786
|
function resolvePreviewSource(config, sourceTable) {
|
|
@@ -676,7 +809,7 @@ async function handlePreviewRequest(config, preview) {
|
|
|
676
809
|
if (!source) return jsonResponse({ error: `No configured source for table "${preview.sourceTable}"` }, 400);
|
|
677
810
|
const idField = source.idField ?? "id";
|
|
678
811
|
const filterColumns = preview.filter ? Object.keys(preview.filter).filter((k) => k !== "$raw") : [];
|
|
679
|
-
const needed = new Set([
|
|
812
|
+
const needed = /* @__PURE__ */ new Set([
|
|
680
813
|
idField,
|
|
681
814
|
...Object.values(preview.fieldMap),
|
|
682
815
|
...filterColumns
|
|
@@ -932,4 +1065,4 @@ function createClivlySDK(config) {
|
|
|
932
1065
|
};
|
|
933
1066
|
}
|
|
934
1067
|
//#endregion
|
|
935
|
-
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK };
|
|
1068
|
+
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ClivlyEntitiesConfig } from "@clivly/core";
|
|
2
|
-
|
|
3
2
|
//#region src/drizzle-meta.d.ts
|
|
4
3
|
declare const DRIZZLE_META: "__clivly_drizzle_source_meta__";
|
|
5
4
|
interface DrizzleSourceMeta {
|
|
@@ -85,9 +84,12 @@ interface ClivlySDKConfig {
|
|
|
85
84
|
* above is unambiguous; nothing here is part of the supported API.
|
|
86
85
|
*/
|
|
87
86
|
experimental?: {
|
|
88
|
-
/** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */
|
|
87
|
+
/** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */
|
|
88
|
+
tunnelToken?: string;
|
|
89
|
+
/** CRM write-back target — reserved. Not wired up yet. */
|
|
89
90
|
writeBack?: {
|
|
90
|
-
enabled?: boolean;
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
/** Only crm_* tables are ever accepted; this narrows further. */
|
|
91
93
|
tables?: string[];
|
|
92
94
|
};
|
|
93
95
|
};
|
|
@@ -116,7 +118,9 @@ interface ClivlySDKConfig {
|
|
|
116
118
|
* responses (e.g. 4xx other than 429) are returned immediately, not retried.
|
|
117
119
|
*/
|
|
118
120
|
retry?: {
|
|
119
|
-
/** Total attempts including the first. Defaults to 3. Min 1. */
|
|
121
|
+
/** Total attempts including the first. Defaults to 3. Min 1. */
|
|
122
|
+
maxAttempts?: number;
|
|
123
|
+
/** Base backoff in ms (doubled each attempt). Defaults to 500. */
|
|
120
124
|
baseDelayMs?: number;
|
|
121
125
|
};
|
|
122
126
|
/** Tables/columns to report for entity discovery + mapping. */
|
|
@@ -126,7 +130,8 @@ interface ClivlySDKConfig {
|
|
|
126
130
|
source?: {
|
|
127
131
|
contacts: SyncSource;
|
|
128
132
|
companies?: SyncSource;
|
|
129
|
-
deals?: SyncSource;
|
|
133
|
+
deals?: SyncSource;
|
|
134
|
+
/** Org-defined custom objects to mirror. Each carries its object-type slug. */
|
|
130
135
|
custom?: Array<SyncSource & {
|
|
131
136
|
objectType: string;
|
|
132
137
|
}>;
|
|
@@ -178,7 +183,9 @@ interface ClivlySDKConfig {
|
|
|
178
183
|
/**
|
|
179
184
|
* Shared secret Clivly Cloud presents (as `Authorization: Bearer <token>`) when
|
|
180
185
|
* calling the `/clivly/auth/verify` endpoint. Required for the verify handler to
|
|
181
|
-
* authenticate the caller; without it the handler fails closed (HTTP 503).
|
|
186
|
+
* authenticate the caller; without it the handler fails closed (HTTP 503). No
|
|
187
|
+
* cloud consumer calls this endpoint yet — it is an inert primitive reserved for
|
|
188
|
+
* a future CRM/dashboard auth-passthrough.
|
|
182
189
|
*/
|
|
183
190
|
verifyToken?: string;
|
|
184
191
|
}
|
|
@@ -201,6 +208,13 @@ interface CreateChatSessionHandlerOptions {
|
|
|
201
208
|
apiKey: string;
|
|
202
209
|
apiUrl?: string;
|
|
203
210
|
fetchImpl?: typeof fetch;
|
|
211
|
+
/**
|
|
212
|
+
* Optional host-auth hook. When provided, it runs on the incoming browser
|
|
213
|
+
* request (session cookies intact) and, on a non-null `ClivlyUser`, the host
|
|
214
|
+
* vouches for that verified identity to Clivly Cloud. Absent/null/throwing =
|
|
215
|
+
* the visitor stays self-reported. Never fails the session.
|
|
216
|
+
*/
|
|
217
|
+
resolveUser?: (req: Request) => Promise<ClivlyUser | null> | ClivlyUser | null;
|
|
204
218
|
}
|
|
205
219
|
interface DoctorCheck {
|
|
206
220
|
/** Human-readable outcome — what passed, or why it failed. */
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ClivlyEntitiesConfig } from "@clivly/core";
|
|
2
|
-
|
|
3
2
|
//#region src/drizzle-meta.d.ts
|
|
4
3
|
declare const DRIZZLE_META: "__clivly_drizzle_source_meta__";
|
|
5
4
|
interface DrizzleSourceMeta {
|
|
@@ -85,9 +84,12 @@ interface ClivlySDKConfig {
|
|
|
85
84
|
* above is unambiguous; nothing here is part of the supported API.
|
|
86
85
|
*/
|
|
87
86
|
experimental?: {
|
|
88
|
-
/** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */
|
|
87
|
+
/** Cloudflare Tunnel token — reserved for tunnel mode. Not wired up yet. */
|
|
88
|
+
tunnelToken?: string;
|
|
89
|
+
/** CRM write-back target — reserved. Not wired up yet. */
|
|
89
90
|
writeBack?: {
|
|
90
|
-
enabled?: boolean;
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
/** Only crm_* tables are ever accepted; this narrows further. */
|
|
91
93
|
tables?: string[];
|
|
92
94
|
};
|
|
93
95
|
};
|
|
@@ -116,7 +118,9 @@ interface ClivlySDKConfig {
|
|
|
116
118
|
* responses (e.g. 4xx other than 429) are returned immediately, not retried.
|
|
117
119
|
*/
|
|
118
120
|
retry?: {
|
|
119
|
-
/** Total attempts including the first. Defaults to 3. Min 1. */
|
|
121
|
+
/** Total attempts including the first. Defaults to 3. Min 1. */
|
|
122
|
+
maxAttempts?: number;
|
|
123
|
+
/** Base backoff in ms (doubled each attempt). Defaults to 500. */
|
|
120
124
|
baseDelayMs?: number;
|
|
121
125
|
};
|
|
122
126
|
/** Tables/columns to report for entity discovery + mapping. */
|
|
@@ -126,7 +130,8 @@ interface ClivlySDKConfig {
|
|
|
126
130
|
source?: {
|
|
127
131
|
contacts: SyncSource;
|
|
128
132
|
companies?: SyncSource;
|
|
129
|
-
deals?: SyncSource;
|
|
133
|
+
deals?: SyncSource;
|
|
134
|
+
/** Org-defined custom objects to mirror. Each carries its object-type slug. */
|
|
130
135
|
custom?: Array<SyncSource & {
|
|
131
136
|
objectType: string;
|
|
132
137
|
}>;
|
|
@@ -178,7 +183,9 @@ interface ClivlySDKConfig {
|
|
|
178
183
|
/**
|
|
179
184
|
* Shared secret Clivly Cloud presents (as `Authorization: Bearer <token>`) when
|
|
180
185
|
* calling the `/clivly/auth/verify` endpoint. Required for the verify handler to
|
|
181
|
-
* authenticate the caller; without it the handler fails closed (HTTP 503).
|
|
186
|
+
* authenticate the caller; without it the handler fails closed (HTTP 503). No
|
|
187
|
+
* cloud consumer calls this endpoint yet — it is an inert primitive reserved for
|
|
188
|
+
* a future CRM/dashboard auth-passthrough.
|
|
182
189
|
*/
|
|
183
190
|
verifyToken?: string;
|
|
184
191
|
}
|
|
@@ -201,6 +208,13 @@ interface CreateChatSessionHandlerOptions {
|
|
|
201
208
|
apiKey: string;
|
|
202
209
|
apiUrl?: string;
|
|
203
210
|
fetchImpl?: typeof fetch;
|
|
211
|
+
/**
|
|
212
|
+
* Optional host-auth hook. When provided, it runs on the incoming browser
|
|
213
|
+
* request (session cookies intact) and, on a non-null `ClivlyUser`, the host
|
|
214
|
+
* vouches for that verified identity to Clivly Cloud. Absent/null/throwing =
|
|
215
|
+
* the visitor stays self-reported. Never fails the session.
|
|
216
|
+
*/
|
|
217
|
+
resolveUser?: (req: Request) => Promise<ClivlyUser | null> | ClivlyUser | null;
|
|
204
218
|
}
|
|
205
219
|
interface DoctorCheck {
|
|
206
220
|
/** Human-readable outcome — what passed, or why it failed. */
|
package/dist/vite.d.cts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { a as ClivlySDK } from "./types-
|
|
1
|
+
import { a as ClivlySDK } from "./types-DTaz5mLi.cjs";
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
|
-
|
|
4
3
|
//#region src/vite.d.ts
|
|
5
4
|
interface ClivlyViteOptions {
|
|
6
5
|
/** Log prefix for Clivly's boot lines. Defaults to "clivly". */
|
package/dist/vite.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { a as ClivlySDK } from "./types-
|
|
1
|
+
import { a as ClivlySDK } from "./types-DTaz5mLi.mjs";
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
|
-
|
|
4
3
|
//#region src/vite.d.ts
|
|
5
4
|
interface ClivlyViteOptions {
|
|
6
5
|
/** Log prefix for Clivly's boot lines. Defaults to "clivly". */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@clivly/core": "0.
|
|
58
|
+
"@clivly/core": "0.7.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"drizzle-orm": ">=0.30",
|
|
61
|
+
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
|
|
62
62
|
"vite": ">=5"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
@@ -72,11 +72,14 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@electric-sql/pglite": "^0.5.4",
|
|
74
74
|
"drizzle-orm": "^0.45.2",
|
|
75
|
-
"tsdown": "^0.
|
|
75
|
+
"tsdown": "^0.22.14",
|
|
76
76
|
"typescript": "^6",
|
|
77
77
|
"vite": "^8.0.8",
|
|
78
78
|
"vitest": "^3.2.0"
|
|
79
79
|
},
|
|
80
|
+
"engines": {
|
|
81
|
+
"node": ">=22"
|
|
82
|
+
},
|
|
80
83
|
"scripts": {
|
|
81
84
|
"build": "tsdown",
|
|
82
85
|
"check-types": "tsc --noEmit",
|
|
File without changes
|
|
File without changes
|