@clivly/sdk 0.1.0 → 0.2.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 +92 -24
- package/dist/drizzle-meta-C0T6F3vL.cjs +9 -0
- package/dist/drizzle-meta-CAJ43cLc.mjs +4 -0
- package/dist/drizzle.cjs +50 -0
- package/dist/drizzle.d.cts +22 -0
- package/dist/drizzle.d.mts +22 -0
- package/dist/drizzle.mjs +49 -0
- package/dist/index.cjs +494 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.mts +19 -124
- package/dist/index.mjs +223 -20
- package/dist/types-B0aSxJ6V.d.cts +224 -0
- package/dist/types-DWt-ytBQ.d.mts +224 -0
- package/package.json +34 -8
package/README.md
CHANGED
|
@@ -14,39 +14,107 @@ npm install @clivly/sdk
|
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import { createClivlySDK } from "@clivly/sdk";
|
|
17
|
-
import {
|
|
17
|
+
import { fromDrizzle } from "@clivly/sdk/drizzle";
|
|
18
|
+
import { defineClivlyConfig } from "@clivly/core";
|
|
18
19
|
import { db, participants, accounts } from "./db";
|
|
19
20
|
|
|
21
|
+
// Declarative entity config — the non-deprecated source of truth. It maps your
|
|
22
|
+
// host tables/columns onto CRM concepts, and `defineClivlyConfig` validates it
|
|
23
|
+
// at load (warning on any non-canonical field key that would mirror as empty).
|
|
24
|
+
const entities = defineClivlyConfig({
|
|
25
|
+
entities: {
|
|
26
|
+
participants: {
|
|
27
|
+
concept: "contact",
|
|
28
|
+
source: "participants",
|
|
29
|
+
fields: { name: "full_name", email: "email" },
|
|
30
|
+
},
|
|
31
|
+
accounts: {
|
|
32
|
+
concept: "company",
|
|
33
|
+
source: "accounts",
|
|
34
|
+
fields: { name: "legal_name", domain: "website" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
20
39
|
const clivly = createClivlySDK({
|
|
21
40
|
apiKey: process.env.CLIVLY_API_KEY!, // sk_live_… from Settings → API keys
|
|
22
|
-
|
|
23
|
-
contactFields: ["id", "full_name", "email", "account_id", "updated_at"],
|
|
41
|
+
entities,
|
|
24
42
|
source: {
|
|
25
|
-
contacts: {
|
|
26
|
-
|
|
27
|
-
cursorField: "updated_at",
|
|
28
|
-
fetchPage: ({ since, limit }) =>
|
|
29
|
-
db.select().from(participants)
|
|
30
|
-
.where(since ? gt(participants.updated_at, since) : undefined)
|
|
31
|
-
.orderBy(participants.updated_at)
|
|
32
|
-
.limit(limit),
|
|
33
|
-
},
|
|
34
|
-
companies: {
|
|
35
|
-
entity: "accounts",
|
|
36
|
-
cursorField: "updated_at",
|
|
37
|
-
fetchPage: ({ since, limit }) =>
|
|
38
|
-
db.select().from(accounts)
|
|
39
|
-
.where(since ? gt(accounts.updated_at, since) : undefined)
|
|
40
|
-
.orderBy(accounts.updated_at)
|
|
41
|
-
.limit(limit),
|
|
42
|
-
},
|
|
43
|
+
contacts: fromDrizzle(db, participants, { entity: "participants", cursorField: "updated_at" }),
|
|
44
|
+
companies: fromDrizzle(db, accounts, { entity: "accounts", cursorField: "updated_at" }),
|
|
43
45
|
},
|
|
44
46
|
sync: { onBoot: true, intervalMs: 30_000, batchSize: 500 },
|
|
45
47
|
});
|
|
46
48
|
|
|
49
|
+
const status = await clivly.connect(); // rich result: { ok, status, reason, retryable, org? }
|
|
50
|
+
if (!status.ok) throw new Error(`Clivly: ${status.reason}`);
|
|
47
51
|
await clivly.start(); // heartbeat + backfill + interval delta sync
|
|
48
|
-
await clivly.sync(); // force a delta sync now
|
|
49
52
|
```
|
|
50
53
|
|
|
51
|
-
`
|
|
52
|
-
|
|
54
|
+
`fromDrizzle` generates the keyset-paginated `fetchPage` the SDK needs, inferring the
|
|
55
|
+
tie-breaker id from the table's primary key — pass `idField` to override. Rows are keyed by
|
|
56
|
+
your source column names; Clivly applies the field map you confirmed during onboarding, so
|
|
57
|
+
re-mapping never needs a redeploy. Non-Drizzle hosts can still pass a hand-written
|
|
58
|
+
`fetchPage` closure instead.
|
|
59
|
+
|
|
60
|
+
> **Legacy:** the flat `contactEntity` / `contactFields` form is still accepted but
|
|
61
|
+
> **deprecated** — prefer the `entities` config above, which is the same model the
|
|
62
|
+
> mapping table and sync engine share.
|
|
63
|
+
|
|
64
|
+
## Verify your setup
|
|
65
|
+
|
|
66
|
+
Put your instance in `clivly.config.ts` (`export default createClivlySDK({...})`), then:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx clivly doctor # env → config → schema → columns → sample fetch → heartbeat
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or in code: `await clivly.doctor()` returns `{ ok, checks }` — the same six read-only checks.
|
|
73
|
+
|
|
74
|
+
## Serverless / edge
|
|
75
|
+
|
|
76
|
+
On request-scoped runtimes (Cloudflare Workers, Vercel, Lambda) there is no long-lived
|
|
77
|
+
process, so `start()`'s `setInterval` keep-alive loop can't persist — it stops ticking after
|
|
78
|
+
the first response. **Don't call `start()` there.** Instead drive one-shot runs from a
|
|
79
|
+
scheduled trigger with `runScheduled()`, or mount `createClivlyHandler()` at a route:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
// Cloudflare Workers
|
|
83
|
+
export default {
|
|
84
|
+
scheduled: () => clivly.runScheduled(), // cron: one heartbeat + one sync
|
|
85
|
+
fetch: clivly.createClivlyHandler(), // or drive via an uptime ping / external scheduler
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`runScheduled()` throws if a sync push fails (so a cron run is marked failed);
|
|
90
|
+
`createClivlyHandler()` returns `200` on success, `500` otherwise. `start()` emits a warning
|
|
91
|
+
if it detects a serverless runtime. Verify a connection from the terminal any time with
|
|
92
|
+
`npx clivly ping`.
|
|
93
|
+
|
|
94
|
+
### Persisting the delta cursor
|
|
95
|
+
|
|
96
|
+
Delta sync tracks a per-entity cursor so each run only pages rows changed since the last one.
|
|
97
|
+
By default that cursor lives in memory — fine for a long-lived process, but **lost on every
|
|
98
|
+
serverless cold start**, so a fresh isolate would re-page the whole entity. Pass a
|
|
99
|
+
`cursorStore` backed by your own KV/DB to persist it:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const clivly = createClivlySDK({
|
|
103
|
+
apiKey: process.env.CLIVLY_API_KEY!,
|
|
104
|
+
entities,
|
|
105
|
+
source: { /* … */ },
|
|
106
|
+
cursorStore: {
|
|
107
|
+
// `SyncCursor` is { id: string; time: Date } — serialize `time` (e.g. ISO string).
|
|
108
|
+
get: async (entity) => {
|
|
109
|
+
const raw = await kv.get(`clivly:cursor:${entity}`);
|
|
110
|
+
return raw ? { id: raw.id, time: new Date(raw.time) } : null;
|
|
111
|
+
},
|
|
112
|
+
set: (entity, cursor) =>
|
|
113
|
+
kv.put(`clivly:cursor:${entity}`, cursor && { id: cursor.id, time: cursor.time.toISOString() }),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Delta sync is **at-least-once**: after a cold start with no persisted cursor, rows since the
|
|
119
|
+
last push may be re-sent (the server dedupes by id) — it never silently skips rows. Use
|
|
120
|
+
`sync({ mode: "full" })` when you need the server to reconcile deletions (archive-on-leave).
|
package/dist/drizzle.cjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_drizzle_meta = require("./drizzle-meta-C0T6F3vL.cjs");
|
|
3
|
+
let drizzle_orm = require("drizzle-orm");
|
|
4
|
+
//#region src/drizzle.ts
|
|
5
|
+
function inferIdField(table) {
|
|
6
|
+
const columns = (0, drizzle_orm.getTableColumns)(table);
|
|
7
|
+
const primaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
8
|
+
if (primaryKeys.length === 1) {
|
|
9
|
+
const primaryKeyField = primaryKeys[0]?.[0];
|
|
10
|
+
if (!primaryKeyField) throw new Error(`fromDrizzle: unable to extract primary key field name from table "${(0, drizzle_orm.getTableName)(table)}".`);
|
|
11
|
+
return primaryKeyField;
|
|
12
|
+
}
|
|
13
|
+
const kind = primaryKeys.length === 0 ? "no" : "a composite";
|
|
14
|
+
throw new Error(`fromDrizzle: table "${(0, drizzle_orm.getTableName)(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
18
|
+
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
19
|
+
* `idField` defaults to the table's single-column primary key.
|
|
20
|
+
*
|
|
21
|
+
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
22
|
+
* `@clivly/sdk/drizzle`.
|
|
23
|
+
*/
|
|
24
|
+
function fromDrizzle(db, table, opts) {
|
|
25
|
+
const drizzleDb = db;
|
|
26
|
+
const idField = opts.idField ?? inferIdField(table);
|
|
27
|
+
const columns = (0, drizzle_orm.getTableColumns)(table);
|
|
28
|
+
const cursorColumn = columns[opts.cursorField];
|
|
29
|
+
const idColumn = columns[idField];
|
|
30
|
+
if (!cursorColumn) throw new Error(`fromDrizzle: cursorField "${opts.cursorField}" is not a column on "${(0, drizzle_orm.getTableName)(table)}".`);
|
|
31
|
+
if (!idColumn) throw new Error(`fromDrizzle: idField "${idField}" is not a column on "${(0, drizzle_orm.getTableName)(table)}".`);
|
|
32
|
+
const source = {
|
|
33
|
+
entity: opts.entity,
|
|
34
|
+
cursorField: opts.cursorField,
|
|
35
|
+
idField,
|
|
36
|
+
fetchPage: ({ since, limit }) => drizzleDb.select().from(table).where(since ? drizzle_orm.sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy((0, drizzle_orm.asc)(cursorColumn), (0, drizzle_orm.asc)(idColumn)).limit(limit)
|
|
37
|
+
};
|
|
38
|
+
const meta = {
|
|
39
|
+
columnKeys: Object.keys(columns),
|
|
40
|
+
cursorField: opts.cursorField,
|
|
41
|
+
idField
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(source, require_drizzle_meta.DRIZZLE_META, {
|
|
44
|
+
value: meta,
|
|
45
|
+
enumerable: false
|
|
46
|
+
});
|
|
47
|
+
return source;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
exports.fromDrizzle = fromDrizzle;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { u as SyncSource } from "./types-B0aSxJ6V.cjs";
|
|
2
|
+
import { Table } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
//#region src/drizzle.d.ts
|
|
5
|
+
interface DrizzleDb {
|
|
6
|
+
select: (...args: any[]) => any;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
10
|
+
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
11
|
+
* `idField` defaults to the table's single-column primary key.
|
|
12
|
+
*
|
|
13
|
+
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
14
|
+
* `@clivly/sdk/drizzle`.
|
|
15
|
+
*/
|
|
16
|
+
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
17
|
+
entity: string;
|
|
18
|
+
cursorField: string;
|
|
19
|
+
idField?: string;
|
|
20
|
+
}): SyncSource;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { fromDrizzle };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { u as SyncSource } from "./types-DWt-ytBQ.mjs";
|
|
2
|
+
import { Table } from "drizzle-orm";
|
|
3
|
+
|
|
4
|
+
//#region src/drizzle.d.ts
|
|
5
|
+
interface DrizzleDb {
|
|
6
|
+
select: (...args: any[]) => any;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
10
|
+
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
11
|
+
* `idField` defaults to the table's single-column primary key.
|
|
12
|
+
*
|
|
13
|
+
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
14
|
+
* `@clivly/sdk/drizzle`.
|
|
15
|
+
*/
|
|
16
|
+
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
17
|
+
entity: string;
|
|
18
|
+
cursorField: string;
|
|
19
|
+
idField?: string;
|
|
20
|
+
}): SyncSource;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { fromDrizzle };
|
package/dist/drizzle.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { t as DRIZZLE_META } from "./drizzle-meta-CAJ43cLc.mjs";
|
|
2
|
+
import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
|
|
3
|
+
//#region src/drizzle.ts
|
|
4
|
+
function inferIdField(table) {
|
|
5
|
+
const columns = getTableColumns(table);
|
|
6
|
+
const primaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
|
|
7
|
+
if (primaryKeys.length === 1) {
|
|
8
|
+
const primaryKeyField = primaryKeys[0]?.[0];
|
|
9
|
+
if (!primaryKeyField) throw new Error(`fromDrizzle: unable to extract primary key field name from table "${getTableName(table)}".`);
|
|
10
|
+
return primaryKeyField;
|
|
11
|
+
}
|
|
12
|
+
const kind = primaryKeys.length === 0 ? "no" : "a composite";
|
|
13
|
+
throw new Error(`fromDrizzle: table "${getTableName(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
17
|
+
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
18
|
+
* `idField` defaults to the table's single-column primary key.
|
|
19
|
+
*
|
|
20
|
+
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
21
|
+
* `@clivly/sdk/drizzle`.
|
|
22
|
+
*/
|
|
23
|
+
function fromDrizzle(db, table, opts) {
|
|
24
|
+
const drizzleDb = db;
|
|
25
|
+
const idField = opts.idField ?? inferIdField(table);
|
|
26
|
+
const columns = getTableColumns(table);
|
|
27
|
+
const cursorColumn = columns[opts.cursorField];
|
|
28
|
+
const idColumn = columns[idField];
|
|
29
|
+
if (!cursorColumn) throw new Error(`fromDrizzle: cursorField "${opts.cursorField}" is not a column on "${getTableName(table)}".`);
|
|
30
|
+
if (!idColumn) throw new Error(`fromDrizzle: idField "${idField}" is not a column on "${getTableName(table)}".`);
|
|
31
|
+
const source = {
|
|
32
|
+
entity: opts.entity,
|
|
33
|
+
cursorField: opts.cursorField,
|
|
34
|
+
idField,
|
|
35
|
+
fetchPage: ({ since, limit }) => drizzleDb.select().from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit)
|
|
36
|
+
};
|
|
37
|
+
const meta = {
|
|
38
|
+
columnKeys: Object.keys(columns),
|
|
39
|
+
cursorField: opts.cursorField,
|
|
40
|
+
idField
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(source, DRIZZLE_META, {
|
|
43
|
+
value: meta,
|
|
44
|
+
enumerable: false
|
|
45
|
+
});
|
|
46
|
+
return source;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { fromDrizzle };
|