@clivly/sdk 0.9.2 → 0.9.4
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 +25 -0
- package/dist/chat-session.d.cts +1 -1
- package/dist/chat-session.d.mts +1 -1
- package/dist/drizzle.cjs +73 -9
- package/dist/drizzle.d.cts +83 -7
- package/dist/drizzle.d.mts +83 -7
- package/dist/drizzle.mjs +73 -10
- package/dist/index.cjs +52 -13
- package/dist/index.d.cts +5 -2
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +52 -14
- package/dist/{types-DxG4rHyo.d.cts → types-Dz1LQYvQ.d.cts} +15 -1
- package/dist/{types-DxG4rHyo.d.mts → types-Dz1LQYvQ.d.mts} +15 -1
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.mts +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -453,6 +453,7 @@ is honoured. Other `4xx` responses return immediately.
|
|
|
453
453
|
| Method | Returns | Notes |
|
|
454
454
|
| --- | --- | --- |
|
|
455
455
|
| `connect()` | `Promise<ClivlyConnectResult>` | One heartbeat, structured. Never throws |
|
|
456
|
+
| `ping()` | `Promise<ClivlyConnectResult>` | Zero-write reachability probe. Same shape as `connect()`, but records no heartbeat — and it alone returns `workspace` |
|
|
456
457
|
| `heartbeat()` | `Promise<boolean>` | One heartbeat, boolean |
|
|
457
458
|
| `start()` | `Promise<void>` | Heartbeat + boot sync + timers. Long-lived hosts only; opens opt-in WebSocket presence |
|
|
458
459
|
| `stop()` | `void` | Clears the timers |
|
|
@@ -461,6 +462,19 @@ is honoured. Other `4xx` responses return immediately.
|
|
|
461
462
|
| `createClivlyHandler()` | `(Request) => Promise<Response>` | Mount at a route |
|
|
462
463
|
| `createAuthVerifyHandler(resolveUser)` | `(Request) => Promise<Response>` | `503` unless `verifyToken` is set |
|
|
463
464
|
| `doctor()` | `Promise<DoctorReport>` | `{ ok, checks }` |
|
|
465
|
+
| `ensureSchema()` | `Promise<EnsureSchemaResult \| null>` | Declares your custom object types **and** the mappings that make them receive rows. `null` = could not deliver; **rejects** if Clivly refuses the config |
|
|
466
|
+
| `ensureCustomObjects()` | `Promise<{ slug, id, created }[] \| null>` | Object types only, no mappings. Superseded by `ensureSchema()` |
|
|
467
|
+
|
|
468
|
+
`clivly push-schema` calls `ensureSchema()` for you, so you rarely need it
|
|
469
|
+
directly — reach for it when you want config-as-code to run from your own
|
|
470
|
+
deploy script or a CI step.
|
|
471
|
+
|
|
472
|
+
The `null`-vs-throw split is deliberate. `null` means the request never landed
|
|
473
|
+
(network, or a Clivly older than the route), which is worth a retry. A rejection
|
|
474
|
+
means Clivly understood the config and declined it — most often because an
|
|
475
|
+
entity would overwrite a mapping someone edited in the dashboard — which is
|
|
476
|
+
worth reading, not retrying. A config-declared mapping is owned by your config;
|
|
477
|
+
a dashboard-edited one is never silently overwritten.
|
|
464
478
|
|
|
465
479
|
### `ClivlyConnectResult.reason`
|
|
466
480
|
|
|
@@ -468,6 +482,17 @@ is honoured. Other `4xx` responses return immediately.
|
|
|
468
482
|
`server_error` (5xx) · `client_error` (other 4xx) · `network_error` (no
|
|
469
483
|
response).
|
|
470
484
|
|
|
485
|
+
### `ClivlyConnectResult.workspace`
|
|
486
|
+
|
|
487
|
+
`ping()` also returns `workspace` — the setup ladder for the org behind your key
|
|
488
|
+
(`IntegrationState` from `@clivly/core`), computed by the server so the CLI and
|
|
489
|
+
the dashboard cannot disagree about how far setup has got. This is what
|
|
490
|
+
`clivly status` prints under **Workspace**.
|
|
491
|
+
|
|
492
|
+
It is `undefined` or `null` when the server predates the field or could not read
|
|
493
|
+
it. Treat that as *unknown*, never as *nothing is set up* — an old server, a
|
|
494
|
+
rejected key and a dead network all mean the question went unanswered.
|
|
495
|
+
|
|
471
496
|
### Subpath exports
|
|
472
497
|
|
|
473
498
|
| From `clivly` | From `@clivly/sdk` | Contents |
|
package/dist/chat-session.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as CreateChatSessionHandlerOptions } from "./types-
|
|
1
|
+
import { c as CreateChatSessionHandlerOptions } from "./types-Dz1LQYvQ.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
|
package/dist/chat-session.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as CreateChatSessionHandlerOptions } from "./types-
|
|
1
|
+
import { c as CreateChatSessionHandlerOptions } from "./types-Dz1LQYvQ.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
|
package/dist/drizzle.cjs
CHANGED
|
@@ -2,6 +2,76 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
|
|
3
3
|
let _clivly_core_drizzle = require("@clivly/core/drizzle");
|
|
4
4
|
let drizzle_orm = require("drizzle-orm");
|
|
5
|
+
let _clivly_core = require("@clivly/core");
|
|
6
|
+
//#region src/drizzle-projection.ts
|
|
7
|
+
/** `<ref>.<column>` — the token `fields` carries and cloud-ingest reads. */
|
|
8
|
+
function refToken(ref) {
|
|
9
|
+
return `${ref.table}.${ref.column}`;
|
|
10
|
+
}
|
|
11
|
+
function refSql(ref) {
|
|
12
|
+
return `${(0, _clivly_core.quoteIdent)(ref.table)}.${(0, _clivly_core.quoteIdent)(ref.column)}`;
|
|
13
|
+
}
|
|
14
|
+
function parseRef(value) {
|
|
15
|
+
if (typeof value !== "string") return value;
|
|
16
|
+
const dot = value.indexOf(".");
|
|
17
|
+
if (dot <= 0 || dot === value.length - 1) throw new Error(`fromDrizzleProjection: cursorField "${value}" must be a ref token like "base.updated_at".`);
|
|
18
|
+
return {
|
|
19
|
+
table: value.slice(0, dot),
|
|
20
|
+
column: value.slice(dot + 1)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Refs address tables symbolically — `base` or a join key — never by table name,
|
|
25
|
+
* which is what stops a projection reaching a table it did not declare. An
|
|
26
|
+
* unresolvable ref would otherwise compile to SQL referencing a missing alias
|
|
27
|
+
* and fail at the database with a message that never mentions the config.
|
|
28
|
+
*/
|
|
29
|
+
function assertKnownRef(ref, known, context) {
|
|
30
|
+
if (!known.has(ref.table)) throw new Error(`fromDrizzleProjection: ${context} names unknown ref "${ref.table}" — expected "${[...known].join("\", \"")}".`);
|
|
31
|
+
}
|
|
32
|
+
function normalizeRows(result) {
|
|
33
|
+
if (Array.isArray(result)) return result;
|
|
34
|
+
const rows = result.rows;
|
|
35
|
+
if (Array.isArray(rows)) return rows;
|
|
36
|
+
throw new Error("fromDrizzleProjection: db.execute() returned neither an array of rows nor { rows }.");
|
|
37
|
+
}
|
|
38
|
+
function fromDrizzleProjection(db, projection, opts) {
|
|
39
|
+
const client = db;
|
|
40
|
+
const known = /* @__PURE__ */ new Set([_clivly_core.BASE_REF, ...projection.joins.map((j) => j.key)]);
|
|
41
|
+
if (projection.identity.kind === "base_pk" && projection.identity.column === void 0 && opts.basePkColumn === void 0) throw new Error(`fromDrizzleProjection: projection "${projection.entityKey}" has a base_pk identity with no column — pass { basePkColumn }.`);
|
|
42
|
+
const cursorRef = parseRef(opts.cursorField);
|
|
43
|
+
assertKnownRef(cursorRef, known, "cursorField");
|
|
44
|
+
for (const [field, binding] of Object.entries(projection.fieldBindings)) assertKnownRef(binding.from, known, `field "${field}"`);
|
|
45
|
+
const identityExpr = (0, _clivly_core.compileProjectionIdentity)(projection, opts.basePkColumn ?? "");
|
|
46
|
+
const cursorToken = refToken(cursorRef);
|
|
47
|
+
const cursorExpr = `CAST(${refSql(cursorRef)} AS timestamptz)`;
|
|
48
|
+
const cursorRead = `to_char(${cursorExpr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
|
|
49
|
+
const selectItems = [`${identityExpr} AS ${(0, _clivly_core.quoteIdent)("id")}`, `${cursorRead} AS ${(0, _clivly_core.quoteIdent)(cursorToken)}`];
|
|
50
|
+
const emitted = /* @__PURE__ */ new Set([cursorToken]);
|
|
51
|
+
for (const binding of Object.values(projection.fieldBindings)) {
|
|
52
|
+
const token = refToken(binding.from);
|
|
53
|
+
if (emitted.has(token)) continue;
|
|
54
|
+
emitted.add(token);
|
|
55
|
+
selectItems.push(`${refSql(binding.from)} AS ${(0, _clivly_core.quoteIdent)(token)}`);
|
|
56
|
+
}
|
|
57
|
+
const head = [
|
|
58
|
+
`select ${selectItems.join(", ")}`,
|
|
59
|
+
(0, _clivly_core.compileProjectionFrom)(projection),
|
|
60
|
+
...(0, _clivly_core.compileProjectionJoins)(projection)
|
|
61
|
+
].join(" ");
|
|
62
|
+
return {
|
|
63
|
+
entity: opts.entity,
|
|
64
|
+
cursorField: cursorToken,
|
|
65
|
+
idField: "id",
|
|
66
|
+
...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
|
|
67
|
+
fetchPage: async ({ since, limit }) => {
|
|
68
|
+
const where = since ? drizzle_orm.sql` where (${drizzle_orm.sql.raw(cursorExpr)}, ${drizzle_orm.sql.raw(identityExpr)}) > (${since.time.toISOString()}::timestamptz, ${since.id})` : drizzle_orm.sql``;
|
|
69
|
+
const query = drizzle_orm.sql`${drizzle_orm.sql.raw(head)}${where} order by ${drizzle_orm.sql.raw(cursorExpr)} asc, ${drizzle_orm.sql.raw(identityExpr)} asc limit ${limit}`;
|
|
70
|
+
return normalizeRows(await client.execute(query));
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
5
75
|
//#region src/drizzle.ts
|
|
6
76
|
function inferIdField(table) {
|
|
7
77
|
const columns = (0, drizzle_orm.getTableColumns)(table);
|
|
@@ -14,14 +84,6 @@ function inferIdField(table) {
|
|
|
14
84
|
const kind = primaryKeys.length === 0 ? "no" : "a composite";
|
|
15
85
|
throw new Error(`fromDrizzle: table "${(0, drizzle_orm.getTableName)(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
|
|
16
86
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
19
|
-
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
20
|
-
* `idField` defaults to the table's single-column primary key.
|
|
21
|
-
*
|
|
22
|
-
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
23
|
-
* `@clivly/sdk/drizzle`.
|
|
24
|
-
*/
|
|
25
87
|
function fromDrizzle(db, table, opts) {
|
|
26
88
|
const drizzleDb = db;
|
|
27
89
|
const idField = opts.idField ?? inferIdField(table);
|
|
@@ -38,7 +100,8 @@ function fromDrizzle(db, table, opts) {
|
|
|
38
100
|
entity: opts.entity,
|
|
39
101
|
cursorField: cursorColumnName,
|
|
40
102
|
idField: idColumnName,
|
|
41
|
-
fetchPage: ({ since, limit }) => drizzleDb.select(projection).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)
|
|
103
|
+
fetchPage: ({ since, limit }) => drizzleDb.select(projection).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),
|
|
104
|
+
...opts.objectType === void 0 ? {} : { objectType: opts.objectType }
|
|
42
105
|
};
|
|
43
106
|
const uniqueConstraints = (0, _clivly_core_drizzle.buildUniqueConstraints)(table);
|
|
44
107
|
const meta = {
|
|
@@ -89,3 +152,4 @@ Object.defineProperty(exports, "discoverFromDrizzle", {
|
|
|
89
152
|
});
|
|
90
153
|
exports.drizzleIntrospector = drizzleIntrospector;
|
|
91
154
|
exports.fromDrizzle = fromDrizzle;
|
|
155
|
+
exports.fromDrizzleProjection = fromDrizzleProjection;
|
package/dist/drizzle.d.cts
CHANGED
|
@@ -1,23 +1,99 @@
|
|
|
1
|
-
import { _ as NamespaceIntrospector, g as SyncSource } from "./types-
|
|
1
|
+
import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Dz1LQYvQ.cjs";
|
|
2
|
+
import { ProjectionMapping, ProjectionRef } from "@clivly/core";
|
|
2
3
|
import { Table } from "drizzle-orm";
|
|
3
4
|
import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
5
|
+
//#region src/drizzle-projection.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Build a `SyncSource` that actually executes a projection's joins.
|
|
8
|
+
*
|
|
9
|
+
* `fromDrizzle` emits a single-table select, so a projection entity paired with
|
|
10
|
+
* it pushed raw base-table rows and every joined field mirrored empty — the
|
|
11
|
+
* declared joins were inert, and the only symptom was a dry run reporting a
|
|
12
|
+
* missing column whose name was correct.
|
|
13
|
+
*
|
|
14
|
+
* The row shape is the contract the cloud already relies on. A projection's
|
|
15
|
+
* `fields` are ref *tokens* (`client_name -> "cl.full_name"`, from
|
|
16
|
+
* `projectionFields`), and `cloud-ingest` reads `row[token]` verbatim plus
|
|
17
|
+
* `row.id` for `sourceRef`. So this aliases every binding by its token rather
|
|
18
|
+
* than by its field name.
|
|
19
|
+
*
|
|
20
|
+
* That is the one deliberate difference from the view compiler, which aliases
|
|
21
|
+
* by field name because the sync engine reads the compiled view directly. FROM,
|
|
22
|
+
* JOIN and the identity expression all come from `@clivly/core`, so both paths
|
|
23
|
+
* join identically and produce the same `id` for the same row.
|
|
24
|
+
*
|
|
25
|
+
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
26
|
+
* `@clivly/sdk/drizzle`. The `db` only needs `.execute()`, the same surface
|
|
27
|
+
* `drizzleIntrospector` requires.
|
|
28
|
+
*/
|
|
29
|
+
interface FromDrizzleProjectionOptions {
|
|
30
|
+
/**
|
|
31
|
+
* The base table's primary key, required only when `identity` is `base_pk`
|
|
32
|
+
* without an explicit column. Unlike `fromDrizzle` there is no `Table` object
|
|
33
|
+
* to infer it from — a projection names tables as strings.
|
|
34
|
+
*/
|
|
35
|
+
basePkColumn?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The column to keyset-paginate on, as a ref token (`"base.updated_at"`) or
|
|
38
|
+
* a `ProjectionRef`. A projection spans several tables, so there is no
|
|
39
|
+
* single table to infer a cursor from.
|
|
40
|
+
*/
|
|
41
|
+
cursorField: string | ProjectionRef;
|
|
42
|
+
/** The source name rows are pushed under. */
|
|
43
|
+
entity: string;
|
|
44
|
+
/**
|
|
45
|
+
* The custom object slug. Accepted here so the result is directly assignable
|
|
46
|
+
* to `source.custom` — bolting it on with a spread afterwards drops the
|
|
47
|
+
* non-enumerable drizzle metadata.
|
|
48
|
+
*/
|
|
49
|
+
objectType?: string;
|
|
50
|
+
}
|
|
51
|
+
declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions & {
|
|
52
|
+
objectType: string;
|
|
53
|
+
}): SyncSource & {
|
|
54
|
+
objectType: string;
|
|
55
|
+
};
|
|
56
|
+
declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions): SyncSource;
|
|
57
|
+
//#endregion
|
|
4
58
|
//#region src/drizzle.d.ts
|
|
5
59
|
interface DrizzleDb {
|
|
6
60
|
select: (...args: any[]) => any;
|
|
7
61
|
}
|
|
62
|
+
interface FromDrizzleOptions {
|
|
63
|
+
cursorField: string;
|
|
64
|
+
entity: string;
|
|
65
|
+
idField?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Custom-object slug, for sources destined for `source.custom`.
|
|
68
|
+
*
|
|
69
|
+
* Pass it here rather than attaching it afterwards. `source.custom` is typed
|
|
70
|
+
* `Array<SyncSource & { objectType: string }>`, so before this option existed
|
|
71
|
+
* the only way to satisfy that type was to bolt the property on post-hoc —
|
|
72
|
+
* and the idiomatic spread (`{ ...fromDrizzle(...), objectType }`) copies only
|
|
73
|
+
* ENUMERABLE own properties, silently dropping the non-enumerable
|
|
74
|
+
* `DRIZZLE_META` below. `doctor` would then report a `fromDrizzle` source as
|
|
75
|
+
* "hand-written" and skip its cursor/id column checks, which is both a lost
|
|
76
|
+
* check and a false explanation.
|
|
77
|
+
*/
|
|
78
|
+
objectType?: string;
|
|
79
|
+
}
|
|
8
80
|
/**
|
|
9
81
|
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
10
82
|
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
11
83
|
* `idField` defaults to the table's single-column primary key.
|
|
12
84
|
*
|
|
85
|
+
* Pass `objectType` for custom objects — the result is then directly assignable
|
|
86
|
+
* to `source.custom` with no spread or mutation.
|
|
87
|
+
*
|
|
13
88
|
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
14
89
|
* `@clivly/sdk/drizzle`.
|
|
15
90
|
*/
|
|
16
|
-
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
91
|
+
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions & {
|
|
92
|
+
objectType: string;
|
|
93
|
+
}): SyncSource & {
|
|
94
|
+
objectType: string;
|
|
95
|
+
};
|
|
96
|
+
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions): SyncSource;
|
|
21
97
|
/**
|
|
22
98
|
* Build a `NamespaceIntrospector` from any Drizzle client that exposes
|
|
23
99
|
* `.execute()`. Reads `information_schema` in the connection's default schema
|
|
@@ -25,4 +101,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
|
25
101
|
*/
|
|
26
102
|
declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
|
|
27
103
|
//#endregion
|
|
28
|
-
export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
|
|
104
|
+
export { type FromDrizzleProjectionOptions, buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
|
package/dist/drizzle.d.mts
CHANGED
|
@@ -1,23 +1,99 @@
|
|
|
1
|
-
import { _ as NamespaceIntrospector, g as SyncSource } from "./types-
|
|
1
|
+
import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Dz1LQYvQ.mjs";
|
|
2
2
|
import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
3
3
|
import { Table } from "drizzle-orm";
|
|
4
|
+
import { ProjectionMapping, ProjectionRef } from "@clivly/core";
|
|
5
|
+
//#region src/drizzle-projection.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Build a `SyncSource` that actually executes a projection's joins.
|
|
8
|
+
*
|
|
9
|
+
* `fromDrizzle` emits a single-table select, so a projection entity paired with
|
|
10
|
+
* it pushed raw base-table rows and every joined field mirrored empty — the
|
|
11
|
+
* declared joins were inert, and the only symptom was a dry run reporting a
|
|
12
|
+
* missing column whose name was correct.
|
|
13
|
+
*
|
|
14
|
+
* The row shape is the contract the cloud already relies on. A projection's
|
|
15
|
+
* `fields` are ref *tokens* (`client_name -> "cl.full_name"`, from
|
|
16
|
+
* `projectionFields`), and `cloud-ingest` reads `row[token]` verbatim plus
|
|
17
|
+
* `row.id` for `sourceRef`. So this aliases every binding by its token rather
|
|
18
|
+
* than by its field name.
|
|
19
|
+
*
|
|
20
|
+
* That is the one deliberate difference from the view compiler, which aliases
|
|
21
|
+
* by field name because the sync engine reads the compiled view directly. FROM,
|
|
22
|
+
* JOIN and the identity expression all come from `@clivly/core`, so both paths
|
|
23
|
+
* join identically and produce the same `id` for the same row.
|
|
24
|
+
*
|
|
25
|
+
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
26
|
+
* `@clivly/sdk/drizzle`. The `db` only needs `.execute()`, the same surface
|
|
27
|
+
* `drizzleIntrospector` requires.
|
|
28
|
+
*/
|
|
29
|
+
interface FromDrizzleProjectionOptions {
|
|
30
|
+
/**
|
|
31
|
+
* The base table's primary key, required only when `identity` is `base_pk`
|
|
32
|
+
* without an explicit column. Unlike `fromDrizzle` there is no `Table` object
|
|
33
|
+
* to infer it from — a projection names tables as strings.
|
|
34
|
+
*/
|
|
35
|
+
basePkColumn?: string;
|
|
36
|
+
/**
|
|
37
|
+
* The column to keyset-paginate on, as a ref token (`"base.updated_at"`) or
|
|
38
|
+
* a `ProjectionRef`. A projection spans several tables, so there is no
|
|
39
|
+
* single table to infer a cursor from.
|
|
40
|
+
*/
|
|
41
|
+
cursorField: string | ProjectionRef;
|
|
42
|
+
/** The source name rows are pushed under. */
|
|
43
|
+
entity: string;
|
|
44
|
+
/**
|
|
45
|
+
* The custom object slug. Accepted here so the result is directly assignable
|
|
46
|
+
* to `source.custom` — bolting it on with a spread afterwards drops the
|
|
47
|
+
* non-enumerable drizzle metadata.
|
|
48
|
+
*/
|
|
49
|
+
objectType?: string;
|
|
50
|
+
}
|
|
51
|
+
declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions & {
|
|
52
|
+
objectType: string;
|
|
53
|
+
}): SyncSource & {
|
|
54
|
+
objectType: string;
|
|
55
|
+
};
|
|
56
|
+
declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions): SyncSource;
|
|
57
|
+
//#endregion
|
|
4
58
|
//#region src/drizzle.d.ts
|
|
5
59
|
interface DrizzleDb {
|
|
6
60
|
select: (...args: any[]) => any;
|
|
7
61
|
}
|
|
62
|
+
interface FromDrizzleOptions {
|
|
63
|
+
cursorField: string;
|
|
64
|
+
entity: string;
|
|
65
|
+
idField?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Custom-object slug, for sources destined for `source.custom`.
|
|
68
|
+
*
|
|
69
|
+
* Pass it here rather than attaching it afterwards. `source.custom` is typed
|
|
70
|
+
* `Array<SyncSource & { objectType: string }>`, so before this option existed
|
|
71
|
+
* the only way to satisfy that type was to bolt the property on post-hoc —
|
|
72
|
+
* and the idiomatic spread (`{ ...fromDrizzle(...), objectType }`) copies only
|
|
73
|
+
* ENUMERABLE own properties, silently dropping the non-enumerable
|
|
74
|
+
* `DRIZZLE_META` below. `doctor` would then report a `fromDrizzle` source as
|
|
75
|
+
* "hand-written" and skip its cursor/id column checks, which is both a lost
|
|
76
|
+
* check and a false explanation.
|
|
77
|
+
*/
|
|
78
|
+
objectType?: string;
|
|
79
|
+
}
|
|
8
80
|
/**
|
|
9
81
|
* Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
|
|
10
82
|
* `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
|
|
11
83
|
* `idField` defaults to the table's single-column primary key.
|
|
12
84
|
*
|
|
85
|
+
* Pass `objectType` for custom objects — the result is then directly assignable
|
|
86
|
+
* to `source.custom` with no spread or mutation.
|
|
87
|
+
*
|
|
13
88
|
* `drizzle-orm` is an optional peer dependency — installed only when you import
|
|
14
89
|
* `@clivly/sdk/drizzle`.
|
|
15
90
|
*/
|
|
16
|
-
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
91
|
+
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions & {
|
|
92
|
+
objectType: string;
|
|
93
|
+
}): SyncSource & {
|
|
94
|
+
objectType: string;
|
|
95
|
+
};
|
|
96
|
+
declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions): SyncSource;
|
|
21
97
|
/**
|
|
22
98
|
* Build a `NamespaceIntrospector` from any Drizzle client that exposes
|
|
23
99
|
* `.execute()`. Reads `information_schema` in the connection's default schema
|
|
@@ -25,4 +101,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
|
|
|
25
101
|
*/
|
|
26
102
|
declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
|
|
27
103
|
//#endregion
|
|
28
|
-
export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
|
|
104
|
+
export { type FromDrizzleProjectionOptions, buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
|
package/dist/drizzle.mjs
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
|
|
2
2
|
import { buildColumnsMeta, buildUniqueConstraints, buildUniqueConstraints as buildUniqueConstraints$1, discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
3
3
|
import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
|
|
4
|
+
import { BASE_REF, compileProjectionFrom, compileProjectionIdentity, compileProjectionJoins, quoteIdent } from "@clivly/core";
|
|
5
|
+
//#region src/drizzle-projection.ts
|
|
6
|
+
/** `<ref>.<column>` — the token `fields` carries and cloud-ingest reads. */
|
|
7
|
+
function refToken(ref) {
|
|
8
|
+
return `${ref.table}.${ref.column}`;
|
|
9
|
+
}
|
|
10
|
+
function refSql(ref) {
|
|
11
|
+
return `${quoteIdent(ref.table)}.${quoteIdent(ref.column)}`;
|
|
12
|
+
}
|
|
13
|
+
function parseRef(value) {
|
|
14
|
+
if (typeof value !== "string") return value;
|
|
15
|
+
const dot = value.indexOf(".");
|
|
16
|
+
if (dot <= 0 || dot === value.length - 1) throw new Error(`fromDrizzleProjection: cursorField "${value}" must be a ref token like "base.updated_at".`);
|
|
17
|
+
return {
|
|
18
|
+
table: value.slice(0, dot),
|
|
19
|
+
column: value.slice(dot + 1)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Refs address tables symbolically — `base` or a join key — never by table name,
|
|
24
|
+
* which is what stops a projection reaching a table it did not declare. An
|
|
25
|
+
* unresolvable ref would otherwise compile to SQL referencing a missing alias
|
|
26
|
+
* and fail at the database with a message that never mentions the config.
|
|
27
|
+
*/
|
|
28
|
+
function assertKnownRef(ref, known, context) {
|
|
29
|
+
if (!known.has(ref.table)) throw new Error(`fromDrizzleProjection: ${context} names unknown ref "${ref.table}" — expected "${[...known].join("\", \"")}".`);
|
|
30
|
+
}
|
|
31
|
+
function normalizeRows(result) {
|
|
32
|
+
if (Array.isArray(result)) return result;
|
|
33
|
+
const rows = result.rows;
|
|
34
|
+
if (Array.isArray(rows)) return rows;
|
|
35
|
+
throw new Error("fromDrizzleProjection: db.execute() returned neither an array of rows nor { rows }.");
|
|
36
|
+
}
|
|
37
|
+
function fromDrizzleProjection(db, projection, opts) {
|
|
38
|
+
const client = db;
|
|
39
|
+
const known = /* @__PURE__ */ new Set([BASE_REF, ...projection.joins.map((j) => j.key)]);
|
|
40
|
+
if (projection.identity.kind === "base_pk" && projection.identity.column === void 0 && opts.basePkColumn === void 0) throw new Error(`fromDrizzleProjection: projection "${projection.entityKey}" has a base_pk identity with no column — pass { basePkColumn }.`);
|
|
41
|
+
const cursorRef = parseRef(opts.cursorField);
|
|
42
|
+
assertKnownRef(cursorRef, known, "cursorField");
|
|
43
|
+
for (const [field, binding] of Object.entries(projection.fieldBindings)) assertKnownRef(binding.from, known, `field "${field}"`);
|
|
44
|
+
const identityExpr = compileProjectionIdentity(projection, opts.basePkColumn ?? "");
|
|
45
|
+
const cursorToken = refToken(cursorRef);
|
|
46
|
+
const cursorExpr = `CAST(${refSql(cursorRef)} AS timestamptz)`;
|
|
47
|
+
const cursorRead = `to_char(${cursorExpr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
|
|
48
|
+
const selectItems = [`${identityExpr} AS ${quoteIdent("id")}`, `${cursorRead} AS ${quoteIdent(cursorToken)}`];
|
|
49
|
+
const emitted = /* @__PURE__ */ new Set([cursorToken]);
|
|
50
|
+
for (const binding of Object.values(projection.fieldBindings)) {
|
|
51
|
+
const token = refToken(binding.from);
|
|
52
|
+
if (emitted.has(token)) continue;
|
|
53
|
+
emitted.add(token);
|
|
54
|
+
selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(token)}`);
|
|
55
|
+
}
|
|
56
|
+
const head = [
|
|
57
|
+
`select ${selectItems.join(", ")}`,
|
|
58
|
+
compileProjectionFrom(projection),
|
|
59
|
+
...compileProjectionJoins(projection)
|
|
60
|
+
].join(" ");
|
|
61
|
+
return {
|
|
62
|
+
entity: opts.entity,
|
|
63
|
+
cursorField: cursorToken,
|
|
64
|
+
idField: "id",
|
|
65
|
+
...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
|
|
66
|
+
fetchPage: async ({ since, limit }) => {
|
|
67
|
+
const where = since ? sql` where (${sql.raw(cursorExpr)}, ${sql.raw(identityExpr)}) > (${since.time.toISOString()}::timestamptz, ${since.id})` : sql``;
|
|
68
|
+
const query = sql`${sql.raw(head)}${where} order by ${sql.raw(cursorExpr)} asc, ${sql.raw(identityExpr)} asc limit ${limit}`;
|
|
69
|
+
return normalizeRows(await client.execute(query));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
4
74
|
//#region src/drizzle.ts
|
|
5
75
|
function inferIdField(table) {
|
|
6
76
|
const columns = getTableColumns(table);
|
|
@@ -13,14 +83,6 @@ function inferIdField(table) {
|
|
|
13
83
|
const kind = primaryKeys.length === 0 ? "no" : "a composite";
|
|
14
84
|
throw new Error(`fromDrizzle: table "${getTableName(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
|
|
15
85
|
}
|
|
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
86
|
function fromDrizzle(db, table, opts) {
|
|
25
87
|
const drizzleDb = db;
|
|
26
88
|
const idField = opts.idField ?? inferIdField(table);
|
|
@@ -37,7 +99,8 @@ function fromDrizzle(db, table, opts) {
|
|
|
37
99
|
entity: opts.entity,
|
|
38
100
|
cursorField: cursorColumnName,
|
|
39
101
|
idField: idColumnName,
|
|
40
|
-
fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit)
|
|
102
|
+
fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit),
|
|
103
|
+
...opts.objectType === void 0 ? {} : { objectType: opts.objectType }
|
|
41
104
|
};
|
|
42
105
|
const uniqueConstraints = buildUniqueConstraints$1(table);
|
|
43
106
|
const meta = {
|
|
@@ -74,4 +137,4 @@ function drizzleIntrospector(db) {
|
|
|
74
137
|
};
|
|
75
138
|
}
|
|
76
139
|
//#endregion
|
|
77
|
-
export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
|
|
140
|
+
export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
|
package/dist/index.cjs
CHANGED
|
@@ -125,11 +125,16 @@ function checkConceptCoverage(config) {
|
|
|
125
125
|
}
|
|
126
126
|
//#endregion
|
|
127
127
|
//#region src/custom-slug.ts
|
|
128
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
128
129
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
129
130
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
130
131
|
function slugify(value) {
|
|
131
132
|
return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
|
|
132
133
|
}
|
|
134
|
+
function suggestionFor(declared) {
|
|
135
|
+
const candidate = slugify(declared);
|
|
136
|
+
return candidate !== declared && SLUG_PATTERN.test(candidate) ? ` Did you mean "${candidate}"?` : "";
|
|
137
|
+
}
|
|
133
138
|
function checkCustomObjectSlugs(config) {
|
|
134
139
|
const custom = config.source?.custom ?? [];
|
|
135
140
|
const declared = config.entities ? (0, _clivly_core_entity_config.customObjectDeclarations)(config.entities) : [];
|
|
@@ -143,12 +148,11 @@ function checkCustomObjectSlugs(config) {
|
|
|
143
148
|
const seen = /* @__PURE__ */ new Map();
|
|
144
149
|
for (const source of custom) {
|
|
145
150
|
const declared = source.objectType;
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
else seen.set(canonical, source.entity);
|
|
151
|
+
if (declared === "") problems.push(`"${source.entity}" has an empty objectType.`);
|
|
152
|
+
else if (!SLUG_PATTERN.test(declared)) problems.push(`"${declared}" is not a valid object type slug — use lowercase letters, digits, "-" or "_", starting with a letter.${suggestionFor(declared)}`);
|
|
153
|
+
const prior = seen.get(declared);
|
|
154
|
+
if (prior && declared !== "") problems.push(`duplicate slug "${declared}" declared by "${prior}" and "${source.entity}".`);
|
|
155
|
+
else seen.set(declared, source.entity);
|
|
152
156
|
}
|
|
153
157
|
if (problems.length > 0) return {
|
|
154
158
|
name: "Custom object slugs",
|
|
@@ -255,10 +259,20 @@ async function runMappingDryRun(config, entityKey) {
|
|
|
255
259
|
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
256
260
|
};
|
|
257
261
|
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
262
|
+
if (missing.length === 0) return {
|
|
263
|
+
name,
|
|
264
|
+
ok: true,
|
|
265
|
+
detail: `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.`
|
|
266
|
+
};
|
|
267
|
+
if (config.entities?.entities?.[entityKey]?.projection !== void 0 && missing.length === Object.keys(fields).length) return {
|
|
268
|
+
name,
|
|
269
|
+
ok: false,
|
|
270
|
+
detail: `Row from "${resolved.source.entity}" carries none of the projection's columns (${missing.join(", ")}) — the source performs no joins, so the projection is declared but never executed. Build it with \`fromDrizzleProjection\` from \`clivly/drizzle\`, which runs the joins and returns rows keyed by ref token.`
|
|
271
|
+
};
|
|
258
272
|
return {
|
|
259
273
|
name,
|
|
260
|
-
ok:
|
|
261
|
-
detail:
|
|
274
|
+
ok: false,
|
|
275
|
+
detail: `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
|
|
262
276
|
};
|
|
263
277
|
}
|
|
264
278
|
//#endregion
|
|
@@ -467,9 +481,27 @@ async function mappingDryRunChecks(config) {
|
|
|
467
481
|
return checks;
|
|
468
482
|
}
|
|
469
483
|
/**
|
|
484
|
+
* Why a source has no `DRIZZLE_META`, without asserting more than we know.
|
|
485
|
+
*
|
|
486
|
+
* There are two ways to get here and they are indistinguishable at runtime: the
|
|
487
|
+
* source really was hand-written, or it came from `fromDrizzle` and a spread
|
|
488
|
+
* stripped the metadata, which is non-enumerable. The second is easy to fall
|
|
489
|
+
* into precisely where `objectType` is present — `source.custom` is typed
|
|
490
|
+
* `Array<SyncSource & { objectType: string }>`, so attaching that property with
|
|
491
|
+
* `{ ...fromDrizzle(...), objectType }` used to be the only way to satisfy it.
|
|
492
|
+
*
|
|
493
|
+
* Flatly calling such a source "hand-written" sent developers looking for one
|
|
494
|
+
* that does not exist. Where `objectType` is present, name the likelier cause
|
|
495
|
+
* and the fix instead of guessing wrong with confidence.
|
|
496
|
+
*/
|
|
497
|
+
function missingMetaDetail(source) {
|
|
498
|
+
const skipped = "column check skipped";
|
|
499
|
+
if (source.objectType === void 0) return `"${source.entity}" is a hand-written source — ${skipped}.`;
|
|
500
|
+
return `"${source.entity}" has no drizzle metadata — ${skipped}. If it came from \`fromDrizzle\`, a spread such as \`{ ...fromDrizzle(...), objectType }\` dropped it; pass \`objectType\` to \`fromDrizzle\` instead.`;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
470
503
|
* Confirm each drizzle source actually has the cursor and id columns the sync
|
|
471
|
-
* pages on.
|
|
472
|
-
* rather than failed.
|
|
504
|
+
* pages on. Sources with no column metadata are skipped rather than failed.
|
|
473
505
|
*/
|
|
474
506
|
function cursorColumnChecks(sources) {
|
|
475
507
|
const checks = [];
|
|
@@ -480,7 +512,7 @@ function cursorColumnChecks(sources) {
|
|
|
480
512
|
name: `Cursor/id columns exist (${source.entity})`,
|
|
481
513
|
ok: false,
|
|
482
514
|
skipped: true,
|
|
483
|
-
detail:
|
|
515
|
+
detail: missingMetaDetail(source)
|
|
484
516
|
});
|
|
485
517
|
continue;
|
|
486
518
|
}
|
|
@@ -1119,11 +1151,16 @@ function observedTriggerUrl(req) {
|
|
|
1119
1151
|
return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
|
|
1120
1152
|
} catch {}
|
|
1121
1153
|
}
|
|
1154
|
+
const PLACEHOLDER_BRACKETS = /[<>]/;
|
|
1155
|
+
function isPlaceholderUrl(value) {
|
|
1156
|
+
return PLACEHOLDER_BRACKETS.test(value);
|
|
1157
|
+
}
|
|
1122
1158
|
/**
|
|
1123
1159
|
* Decide which trigger URL to report, in precedence order:
|
|
1124
1160
|
*
|
|
1125
1161
|
* 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
|
|
1126
|
-
* purpose, so it beats anything inferred.
|
|
1162
|
+
* purpose, so it beats anything inferred. An unedited placeholder is not
|
|
1163
|
+
* "on purpose" and is skipped.
|
|
1127
1164
|
* 2. platform env origin + `configuredPath` — available at boot, before any
|
|
1128
1165
|
* request has arrived. Needs the path because platform variables only give
|
|
1129
1166
|
* an origin.
|
|
@@ -1133,7 +1170,7 @@ function observedTriggerUrl(req) {
|
|
|
1133
1170
|
* doesn't need to know about it.
|
|
1134
1171
|
*/
|
|
1135
1172
|
function resolveTriggerUrl(args) {
|
|
1136
|
-
if (args.configuredUrl) return args.configuredUrl;
|
|
1173
|
+
if (args.configuredUrl && !isPlaceholderUrl(args.configuredUrl)) return args.configuredUrl;
|
|
1137
1174
|
const origin = originFromPlatformEnv(args.env);
|
|
1138
1175
|
if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
|
|
1139
1176
|
return args.observedUrl;
|
|
@@ -1702,6 +1739,7 @@ function createClivlySDK(config) {
|
|
|
1702
1739
|
reason: "ok",
|
|
1703
1740
|
retryable: false,
|
|
1704
1741
|
org: body?.org,
|
|
1742
|
+
workspace: body?.workspace ?? null,
|
|
1705
1743
|
..."appLastSeenAt" in body ? { appLastSeenAt: body.appLastSeenAt ?? null } : {}
|
|
1706
1744
|
};
|
|
1707
1745
|
}
|
|
@@ -1891,6 +1929,7 @@ Object.defineProperty(exports, "defineClivlyConfig", {
|
|
|
1891
1929
|
return _clivly_core_entity_config.defineClivlyConfig;
|
|
1892
1930
|
}
|
|
1893
1931
|
});
|
|
1932
|
+
exports.isPlaceholderUrl = isPlaceholderUrl;
|
|
1894
1933
|
Object.defineProperty(exports, "validateEntitiesConfig", {
|
|
1895
1934
|
enumerable: true,
|
|
1896
1935
|
get: function() {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, 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, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-Dz1LQYvQ.cjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.cjs";
|
|
3
3
|
import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
|
+
//#region src/trigger-url.d.ts
|
|
5
|
+
declare function isPlaceholderUrl(value: string): boolean;
|
|
6
|
+
//#endregion
|
|
4
7
|
//#region src/errors.d.ts
|
|
5
8
|
declare class ClivlyError extends Error {
|
|
6
9
|
/** HTTP status, or null when the request never completed (network error). */
|
|
@@ -31,4 +34,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
|
|
|
31
34
|
*/
|
|
32
35
|
declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
|
|
33
36
|
//#endregion
|
|
34
|
-
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
|
|
37
|
+
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, 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, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-Dz1LQYvQ.mjs";
|
|
2
2
|
import { createChatSessionHandler } from "./chat-session.mjs";
|
|
3
3
|
import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
|
|
4
|
+
//#region src/trigger-url.d.ts
|
|
5
|
+
declare function isPlaceholderUrl(value: string): boolean;
|
|
6
|
+
//#endregion
|
|
4
7
|
//#region src/errors.d.ts
|
|
5
8
|
declare class ClivlyError extends Error {
|
|
6
9
|
/** HTTP status, or null when the request never completed (network error). */
|
|
@@ -31,4 +34,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
|
|
|
31
34
|
*/
|
|
32
35
|
declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
|
|
33
36
|
//#endregion
|
|
34
|
-
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
|
|
37
|
+
export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -124,11 +124,16 @@ function checkConceptCoverage(config) {
|
|
|
124
124
|
}
|
|
125
125
|
//#endregion
|
|
126
126
|
//#region src/custom-slug.ts
|
|
127
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
127
128
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
128
129
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
129
130
|
function slugify(value) {
|
|
130
131
|
return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
|
|
131
132
|
}
|
|
133
|
+
function suggestionFor(declared) {
|
|
134
|
+
const candidate = slugify(declared);
|
|
135
|
+
return candidate !== declared && SLUG_PATTERN.test(candidate) ? ` Did you mean "${candidate}"?` : "";
|
|
136
|
+
}
|
|
132
137
|
function checkCustomObjectSlugs(config) {
|
|
133
138
|
const custom = config.source?.custom ?? [];
|
|
134
139
|
const declared = config.entities ? customObjectDeclarations$1(config.entities) : [];
|
|
@@ -142,12 +147,11 @@ function checkCustomObjectSlugs(config) {
|
|
|
142
147
|
const seen = /* @__PURE__ */ new Map();
|
|
143
148
|
for (const source of custom) {
|
|
144
149
|
const declared = source.objectType;
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
else seen.set(canonical, source.entity);
|
|
150
|
+
if (declared === "") problems.push(`"${source.entity}" has an empty objectType.`);
|
|
151
|
+
else if (!SLUG_PATTERN.test(declared)) problems.push(`"${declared}" is not a valid object type slug — use lowercase letters, digits, "-" or "_", starting with a letter.${suggestionFor(declared)}`);
|
|
152
|
+
const prior = seen.get(declared);
|
|
153
|
+
if (prior && declared !== "") problems.push(`duplicate slug "${declared}" declared by "${prior}" and "${source.entity}".`);
|
|
154
|
+
else seen.set(declared, source.entity);
|
|
151
155
|
}
|
|
152
156
|
if (problems.length > 0) return {
|
|
153
157
|
name: "Custom object slugs",
|
|
@@ -254,10 +258,20 @@ async function runMappingDryRun(config, entityKey) {
|
|
|
254
258
|
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
255
259
|
};
|
|
256
260
|
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
261
|
+
if (missing.length === 0) return {
|
|
262
|
+
name,
|
|
263
|
+
ok: true,
|
|
264
|
+
detail: `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.`
|
|
265
|
+
};
|
|
266
|
+
if (config.entities?.entities?.[entityKey]?.projection !== void 0 && missing.length === Object.keys(fields).length) return {
|
|
267
|
+
name,
|
|
268
|
+
ok: false,
|
|
269
|
+
detail: `Row from "${resolved.source.entity}" carries none of the projection's columns (${missing.join(", ")}) — the source performs no joins, so the projection is declared but never executed. Build it with \`fromDrizzleProjection\` from \`clivly/drizzle\`, which runs the joins and returns rows keyed by ref token.`
|
|
270
|
+
};
|
|
257
271
|
return {
|
|
258
272
|
name,
|
|
259
|
-
ok:
|
|
260
|
-
detail:
|
|
273
|
+
ok: false,
|
|
274
|
+
detail: `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
|
|
261
275
|
};
|
|
262
276
|
}
|
|
263
277
|
//#endregion
|
|
@@ -466,9 +480,27 @@ async function mappingDryRunChecks(config) {
|
|
|
466
480
|
return checks;
|
|
467
481
|
}
|
|
468
482
|
/**
|
|
483
|
+
* Why a source has no `DRIZZLE_META`, without asserting more than we know.
|
|
484
|
+
*
|
|
485
|
+
* There are two ways to get here and they are indistinguishable at runtime: the
|
|
486
|
+
* source really was hand-written, or it came from `fromDrizzle` and a spread
|
|
487
|
+
* stripped the metadata, which is non-enumerable. The second is easy to fall
|
|
488
|
+
* into precisely where `objectType` is present — `source.custom` is typed
|
|
489
|
+
* `Array<SyncSource & { objectType: string }>`, so attaching that property with
|
|
490
|
+
* `{ ...fromDrizzle(...), objectType }` used to be the only way to satisfy it.
|
|
491
|
+
*
|
|
492
|
+
* Flatly calling such a source "hand-written" sent developers looking for one
|
|
493
|
+
* that does not exist. Where `objectType` is present, name the likelier cause
|
|
494
|
+
* and the fix instead of guessing wrong with confidence.
|
|
495
|
+
*/
|
|
496
|
+
function missingMetaDetail(source) {
|
|
497
|
+
const skipped = "column check skipped";
|
|
498
|
+
if (source.objectType === void 0) return `"${source.entity}" is a hand-written source — ${skipped}.`;
|
|
499
|
+
return `"${source.entity}" has no drizzle metadata — ${skipped}. If it came from \`fromDrizzle\`, a spread such as \`{ ...fromDrizzle(...), objectType }\` dropped it; pass \`objectType\` to \`fromDrizzle\` instead.`;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
469
502
|
* Confirm each drizzle source actually has the cursor and id columns the sync
|
|
470
|
-
* pages on.
|
|
471
|
-
* rather than failed.
|
|
503
|
+
* pages on. Sources with no column metadata are skipped rather than failed.
|
|
472
504
|
*/
|
|
473
505
|
function cursorColumnChecks(sources) {
|
|
474
506
|
const checks = [];
|
|
@@ -479,7 +511,7 @@ function cursorColumnChecks(sources) {
|
|
|
479
511
|
name: `Cursor/id columns exist (${source.entity})`,
|
|
480
512
|
ok: false,
|
|
481
513
|
skipped: true,
|
|
482
|
-
detail:
|
|
514
|
+
detail: missingMetaDetail(source)
|
|
483
515
|
});
|
|
484
516
|
continue;
|
|
485
517
|
}
|
|
@@ -1118,11 +1150,16 @@ function observedTriggerUrl(req) {
|
|
|
1118
1150
|
return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
|
|
1119
1151
|
} catch {}
|
|
1120
1152
|
}
|
|
1153
|
+
const PLACEHOLDER_BRACKETS = /[<>]/;
|
|
1154
|
+
function isPlaceholderUrl(value) {
|
|
1155
|
+
return PLACEHOLDER_BRACKETS.test(value);
|
|
1156
|
+
}
|
|
1121
1157
|
/**
|
|
1122
1158
|
* Decide which trigger URL to report, in precedence order:
|
|
1123
1159
|
*
|
|
1124
1160
|
* 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
|
|
1125
|
-
* purpose, so it beats anything inferred.
|
|
1161
|
+
* purpose, so it beats anything inferred. An unedited placeholder is not
|
|
1162
|
+
* "on purpose" and is skipped.
|
|
1126
1163
|
* 2. platform env origin + `configuredPath` — available at boot, before any
|
|
1127
1164
|
* request has arrived. Needs the path because platform variables only give
|
|
1128
1165
|
* an origin.
|
|
@@ -1132,7 +1169,7 @@ function observedTriggerUrl(req) {
|
|
|
1132
1169
|
* doesn't need to know about it.
|
|
1133
1170
|
*/
|
|
1134
1171
|
function resolveTriggerUrl(args) {
|
|
1135
|
-
if (args.configuredUrl) return args.configuredUrl;
|
|
1172
|
+
if (args.configuredUrl && !isPlaceholderUrl(args.configuredUrl)) return args.configuredUrl;
|
|
1136
1173
|
const origin = originFromPlatformEnv(args.env);
|
|
1137
1174
|
if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
|
|
1138
1175
|
return args.observedUrl;
|
|
@@ -1701,6 +1738,7 @@ function createClivlySDK(config) {
|
|
|
1701
1738
|
reason: "ok",
|
|
1702
1739
|
retryable: false,
|
|
1703
1740
|
org: body?.org,
|
|
1741
|
+
workspace: body?.workspace ?? null,
|
|
1704
1742
|
..."appLastSeenAt" in body ? { appLastSeenAt: body.appLastSeenAt ?? null } : {}
|
|
1705
1743
|
};
|
|
1706
1744
|
}
|
|
@@ -1872,4 +1910,4 @@ function createClivlySDK(config) {
|
|
|
1872
1910
|
};
|
|
1873
1911
|
}
|
|
1874
1912
|
//#endregion
|
|
1875
|
-
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
|
|
1913
|
+
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClivlyEntitiesConfig } from "@clivly/core";
|
|
1
|
+
import { ClivlyEntitiesConfig, IntegrationState } from "@clivly/core";
|
|
2
2
|
//#region src/drizzle-meta.d.ts
|
|
3
3
|
declare const DRIZZLE_META: "__clivly_drizzle_source_meta__";
|
|
4
4
|
interface DrizzleSourceMeta {
|
|
@@ -309,6 +309,12 @@ interface ClivlyConnectResult {
|
|
|
309
309
|
retryable: boolean;
|
|
310
310
|
/** HTTP status, or null when the request never completed (network error). */
|
|
311
311
|
status: number | null;
|
|
312
|
+
/**
|
|
313
|
+
* The workspace's setup ladder, computed by the server. Present only on
|
|
314
|
+
* `ping()`, and null when the server is older than this field or could not
|
|
315
|
+
* read it — which the CLI renders as "unavailable", never as "not set up".
|
|
316
|
+
*/
|
|
317
|
+
workspace?: IntegrationState | null;
|
|
312
318
|
}
|
|
313
319
|
interface SyncCursor {
|
|
314
320
|
/** The row's id, breaking ties when timestamps collide. */
|
|
@@ -325,6 +331,14 @@ interface SyncSource {
|
|
|
325
331
|
}): Promise<Record<string, unknown>[]>;
|
|
326
332
|
/** Stable, unique tie-breaker column for keyset paging. Defaults to "id". */
|
|
327
333
|
idField?: string;
|
|
334
|
+
/**
|
|
335
|
+
* Custom-object slug this source mirrors into. Only meaningful for entries in
|
|
336
|
+
* `source.custom`, whose element type narrows it to required — declared here
|
|
337
|
+
* because the sync and readiness paths read `source.objectType` off a plain
|
|
338
|
+
* `SyncSource`, so leaving it off left the property required by one type,
|
|
339
|
+
* absent from another, and read by the runtime.
|
|
340
|
+
*/
|
|
341
|
+
objectType?: string;
|
|
328
342
|
/** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
|
|
329
343
|
[DRIZZLE_META]?: DrizzleSourceMeta;
|
|
330
344
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClivlyEntitiesConfig } from "@clivly/core";
|
|
1
|
+
import { ClivlyEntitiesConfig, IntegrationState } from "@clivly/core";
|
|
2
2
|
//#region src/drizzle-meta.d.ts
|
|
3
3
|
declare const DRIZZLE_META: "__clivly_drizzle_source_meta__";
|
|
4
4
|
interface DrizzleSourceMeta {
|
|
@@ -309,6 +309,12 @@ interface ClivlyConnectResult {
|
|
|
309
309
|
retryable: boolean;
|
|
310
310
|
/** HTTP status, or null when the request never completed (network error). */
|
|
311
311
|
status: number | null;
|
|
312
|
+
/**
|
|
313
|
+
* The workspace's setup ladder, computed by the server. Present only on
|
|
314
|
+
* `ping()`, and null when the server is older than this field or could not
|
|
315
|
+
* read it — which the CLI renders as "unavailable", never as "not set up".
|
|
316
|
+
*/
|
|
317
|
+
workspace?: IntegrationState | null;
|
|
312
318
|
}
|
|
313
319
|
interface SyncCursor {
|
|
314
320
|
/** The row's id, breaking ties when timestamps collide. */
|
|
@@ -325,6 +331,14 @@ interface SyncSource {
|
|
|
325
331
|
}): Promise<Record<string, unknown>[]>;
|
|
326
332
|
/** Stable, unique tie-breaker column for keyset paging. Defaults to "id". */
|
|
327
333
|
idField?: string;
|
|
334
|
+
/**
|
|
335
|
+
* Custom-object slug this source mirrors into. Only meaningful for entries in
|
|
336
|
+
* `source.custom`, whose element type narrows it to required — declared here
|
|
337
|
+
* because the sync and readiness paths read `source.objectType` off a plain
|
|
338
|
+
* `SyncSource`, so leaving it off left the property required by one type,
|
|
339
|
+
* absent from another, and read by the runtime.
|
|
340
|
+
*/
|
|
341
|
+
objectType?: string;
|
|
328
342
|
/** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
|
|
329
343
|
[DRIZZLE_META]?: DrizzleSourceMeta;
|
|
330
344
|
}
|
package/dist/vite.d.cts
CHANGED
package/dist/vite.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"access": "public"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@clivly/core": "0.9.
|
|
58
|
+
"@clivly/core": "0.9.4"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
|