@clivly/sdk 0.9.0 → 0.9.2

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 CHANGED
@@ -1,9 +1,10 @@
1
1
  # @clivly/sdk
2
2
 
3
- Connect your app to Clivly Cloud. The SDK sends a heartbeat, reports your schema
4
- for entity mapping, and mirrors your contacts and companies into Clivly over
5
- outbound HTTPS. If you configure `syncTrigger`, Clivly can also call back into
6
- your app to start a sync from the dashboard.
3
+ Connect your app to Clivly Cloud. The SDK reports your schema for entity mapping
4
+ and mirrors your contacts and companies over outbound HTTPS. Long-lived Node
5
+ hosts can opt into WebSocket presence, which replaces the periodic HTTP
6
+ liveness heartbeat while the socket is ready and falls back automatically if it
7
+ is not.
7
8
 
8
9
  Clivly never connects to your database — the SDK runs inside your app and is the
9
10
  only thing that touches your data.
@@ -69,6 +70,9 @@ export default createClivlySDK({
69
70
  cursorField: "updatedAt",
70
71
  }),
71
72
  },
73
+ presence: {
74
+ transport: "websocket", // long-lived Node hosts only; HTTP fallback is automatic
75
+ },
72
76
  });
73
77
  ```
74
78
 
@@ -96,13 +100,44 @@ The single most consequential decision in the setup.
96
100
 
97
101
  | Host runtime | Use | Why |
98
102
  | --- | --- | --- |
99
- | Long-lived Node server, Vite dev | `clivlyVite(sdk)` or `await sdk.start()` | The `setInterval` keep-alive loop persists |
103
+ | Long-lived Node server, Vite dev | `clivlyVite(sdk)` or `await sdk.start()` | The keep-alive loop persists; optionally use WebSocket presence |
100
104
  | Cloudflare Workers, Vercel, Lambda | `sdk.createClivlyHandler()` at a route, and/or `sdk.runScheduled()` from cron | `setInterval` does **not** survive between invocations |
101
105
  | One-off health check | `await sdk.connect()` | Structured `{ ok, status, reason, retryable, org? }` |
102
106
 
103
107
  `start()` emits a console warning when it detects a serverless runtime, because
104
108
  there it stops ticking after the first response.
105
109
 
110
+ ### WebSocket presence
111
+
112
+ HTTP is the default presence transport. To replace the 60-second HTTP liveness
113
+ cadence on a long-lived Node host, opt in:
114
+
115
+ ```ts
116
+ createClivlySDK({
117
+ // ...the rest of your configuration
118
+ presence: {
119
+ transport: "websocket",
120
+ deploymentId: process.env.CLIVLY_DEPLOYMENT_ID, // optional
121
+ },
122
+ });
123
+ ```
124
+
125
+ `start()` performs its initial HTTP check-in, then opens an authenticated
126
+ outbound socket. When the server confirms it is ready, the socket becomes the
127
+ liveness signal and the periodic HTTP heartbeat pauses. A closed or unavailable
128
+ socket reconnects with backoff while the SDK resumes HTTP heartbeat
129
+ automatically. This does not alter `sync.intervalMs`; it only changes presence.
130
+
131
+ Keep the default HTTP transport for serverless and edge runtimes. They cannot
132
+ retain a WebSocket across invocations, so use `runScheduled()` or
133
+ `createClivlyHandler()` there.
134
+
135
+ For rollout reliability, enabled WebSocket presence emits bounded lifecycle
136
+ metrics (event, transport, outcome, state, duration and attempt). It never
137
+ includes credentials, organization or deployment identifiers, host URLs, row
138
+ data, error messages, or stack traces. See
139
+ [SECURITY.md](../../SECURITY.md#telemetry).
140
+
106
141
  ### Entities vs. sources — the distinction that bites
107
142
 
108
143
  ```ts
@@ -394,10 +429,12 @@ is honoured. Other `4xx` responses return immediately.
394
429
  | `apiKey` | `string` | — | **Required.** `sk_live_…` |
395
430
  | `apiUrl` | `string` | `https://api.clivly.com` | Non-default backend |
396
431
  | `entities` | `ClivlyEntitiesConfig` | — | From `defineClivlyConfig({ entities })` |
397
- | `schema` | `DiscoveredTable[]` | — | Use `discoverFromDrizzle(schema)` |
432
+ | `schema` | `DiscoveredTable[]` | — | Use `discoverFromDrizzle(schema)`. It reports keys, types, nullability and FK edges alongside column names; [derived objects](#derived-objects) cannot be authored without them, so prefer it over a hand-written array |
398
433
  | `source` | `{ contacts, companies?, deals?, custom? }` | — | Keyed by CRM concept. `contacts` is required when `source` is present |
399
434
  | `cursorStore` | `CursorStore` | in-memory | Persist on serverless |
400
- | `heartbeatIntervalMs` | `number` | `60000` | |
435
+ | `heartbeatIntervalMs` | `number` | `60000` | HTTP presence cadence and default sync cadence; paused when opt-in WebSocket presence is ready |
436
+ | `presence.transport` | `"http" \\| "websocket"` | `"http"` | WebSocket is for long-lived Node hosts; HTTP resumes during reconnect/fallback |
437
+ | `presence.deploymentId` | `string` | platform-derived | Optional release identity for a WebSocket deployment |
401
438
  | `sync.onBoot` | `boolean` | `true` | Only affects `start()` |
402
439
  | `sync.intervalMs` | `number` | `heartbeatIntervalMs` | |
403
440
  | `sync.batchSize` | `number` | `500` | Rows per page |
@@ -417,7 +454,7 @@ is honoured. Other `4xx` responses return immediately.
417
454
  | --- | --- | --- |
418
455
  | `connect()` | `Promise<ClivlyConnectResult>` | One heartbeat, structured. Never throws |
419
456
  | `heartbeat()` | `Promise<boolean>` | One heartbeat, boolean |
420
- | `start()` | `Promise<void>` | Heartbeat + boot sync + timers. Long-lived hosts only |
457
+ | `start()` | `Promise<void>` | Heartbeat + boot sync + timers. Long-lived hosts only; opens opt-in WebSocket presence |
421
458
  | `stop()` | `void` | Clears the timers |
422
459
  | `sync(options?)` | `Promise<void>` | `{ mode?: "delta" \| "full", runId?: string }`. Throws on push failure |
423
460
  | `runScheduled(options?)` | `Promise<void>` | One heartbeat + one sync, no timers. Throws on push failure |
@@ -436,14 +473,44 @@ response).
436
473
  | From `clivly` | From `@clivly/sdk` | Contents |
437
474
  | --- | --- | --- |
438
475
  | `clivly/sdk` | `@clivly/sdk` | `createClivlySDK`, `defineClivlyConfig`, `createChatSessionHandler`, error classes, all types |
439
- | `clivly/drizzle` | `@clivly/sdk/drizzle` | `fromDrizzle`, `discoverFromDrizzle`, `drizzleIntrospector` |
476
+ | `clivly/drizzle` | `@clivly/sdk/drizzle` | `fromDrizzle`, `discoverFromDrizzle`, `drizzleIntrospector`, `buildUniqueConstraints` |
440
477
  | `clivly/vite` | `@clivly/sdk/vite` | `clivlyVite` |
441
478
  | — | `@clivly/sdk/chat-session` | `createChatSessionHandler` (also on the root) |
442
479
 
480
+ ## Derived objects
481
+
482
+ A custom object can be derived from a base table plus explicit joins, instead of
483
+ mapping exactly one table — an `Enrollment` built from `enrollments` joined to
484
+ `students` and `courses`. You declare one on an entity in `clivly.config.ts`,
485
+ alongside the object it materializes into; `validateEntitiesConfig` checks it in
486
+ CI.
487
+
488
+ Two things have to line up:
489
+
490
+ - **Discovery must be rich enough.** `discoverFromDrizzle` sends primary keys,
491
+ unique constraints, nullability, column types and foreign-key edges.
492
+ Validation is fail-closed, so if your config hand-writes `schema` as
493
+ `{ name, columns }` pairs, every table is rejected with *"has no key/column
494
+ metadata — upgrade `@clivly/sdk` and re-run discovery"*. Pass
495
+ `discoverFromDrizzle(schema)` and redeploy so a fresh heartbeat carries the
496
+ richer payload — and re-run it after schema changes, since projections
497
+ validate against the last reported snapshot.
498
+ - **The server must have the capability enabled.** Authoring sits behind
499
+ `CLIVLY_PROJECTION_MAPPINGS=true` while the surface settles. Reading and
500
+ syncing an existing derived object is never gated.
501
+
502
+ > Requires `clivly@0.9.0` / `@clivly/sdk@0.9.0` or later for the discovery
503
+ > metadata. Earlier versions reported names only, so no host app could author a
504
+ > derived object.
505
+
506
+ Full guide: [`docs/guides/derived-objects.md`](../../docs/guides/derived-objects.md).
507
+
443
508
  ## Known limitations
444
509
 
445
510
  - `start()` is for long-lived runtimes only. On serverless/edge, use
446
511
  `runScheduled()` or `createClivlyHandler()`.
512
+ - WebSocket presence is opt-in and long-lived Node-only. It does not replace
513
+ HTTP presence on serverless or edge hosts.
447
514
  - `fromDrizzle` is the only generated source path. Non-Drizzle hosts write
448
515
  `fetchPage` by hand.
449
516
  - The flat `contactEntity` / `contactFields` config still works but is
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-QPbki8ur.cjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-DxG4rHyo.cjs";
2
2
  //#region src/chat-session.d.ts
3
3
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-QPbki8ur.mjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-DxG4rHyo.mjs";
2
2
  //#region src/chat-session.d.ts
3
3
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { m as NamespaceIntrospector, p as SyncSource } from "./types-QPbki8ur.cjs";
1
+ import { _ as NamespaceIntrospector, g as SyncSource } from "./types-DxG4rHyo.cjs";
2
2
  import { Table } from "drizzle-orm";
3
3
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
4
4
  //#region src/drizzle.d.ts
@@ -1,4 +1,4 @@
1
- import { m as NamespaceIntrospector, p as SyncSource } from "./types-QPbki8ur.mjs";
1
+ import { _ as NamespaceIntrospector, g as SyncSource } from "./types-DxG4rHyo.mjs";
2
2
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
3
3
  import { Table } from "drizzle-orm";
4
4
  //#region src/drizzle.d.ts