@clivly/sdk 0.1.0 → 0.3.0-next.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 CHANGED
@@ -1,52 +1,222 @@
1
1
  # @clivly/sdk
2
2
 
3
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 only (no inbound ports, no tunnel).
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.
7
+
8
+ For the end-to-end onboarding flow, start with
9
+ [`docs/guides/connecting-to-clivly-cloud.md`](../../docs/guides/connecting-to-clivly-cloud.md).
10
+ This README stays focused on the SDK surface itself.
6
11
 
7
12
  ## Install
8
13
 
9
14
  ```bash
10
- npm install @clivly/sdk
15
+ npm install @clivly/sdk @clivly/core clivly
11
16
  ```
12
17
 
18
+ Add `drizzle-orm` when you use `@clivly/sdk/drizzle`, and `vite` when you use
19
+ the Vite/TanStack Start plugin.
20
+
13
21
  ## Usage
14
22
 
15
23
  ```ts
16
24
  import { createClivlySDK } from "@clivly/sdk";
17
- import { and, gt } from "drizzle-orm";
25
+ import { fromDrizzle } from "@clivly/sdk/drizzle";
26
+ import { defineClivlyConfig } from "@clivly/core";
18
27
  import { db, participants, accounts } from "./db";
19
28
 
29
+ // Declarative entity config — the non-deprecated source of truth. It maps your
30
+ // host tables/columns onto CRM concepts, and `defineClivlyConfig` validates it
31
+ // at load (warning on any non-canonical field key that would mirror as empty).
32
+ const entities = defineClivlyConfig({
33
+ entities: {
34
+ participants: {
35
+ concept: "contact",
36
+ source: "participants",
37
+ fields: { name: "full_name", email: "email" },
38
+ },
39
+ accounts: {
40
+ concept: "company",
41
+ source: "accounts",
42
+ fields: { name: "legal_name", domain: "website" },
43
+ },
44
+ },
45
+ });
46
+
20
47
  const clivly = createClivlySDK({
21
48
  apiKey: process.env.CLIVLY_API_KEY!, // sk_live_… from Settings → API keys
22
- contactEntity: "participants",
23
- contactFields: ["id", "full_name", "email", "account_id", "updated_at"],
49
+ entities,
24
50
  source: {
25
- contacts: {
26
- entity: "participants",
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
- },
51
+ contacts: fromDrizzle(db, participants, { entity: "participants", cursorField: "updatedAt" }),
52
+ companies: fromDrizzle(db, accounts, { entity: "accounts", cursorField: "updatedAt" }),
43
53
  },
44
54
  sync: { onBoot: true, intervalMs: 30_000, batchSize: 500 },
45
55
  });
46
56
 
57
+ const status = await clivly.connect(); // rich result: { ok, status, reason, retryable, org? }
58
+ if (!status.ok) throw new Error(`Clivly: ${status.reason}`);
47
59
  await clivly.start(); // heartbeat + backfill + interval delta sync
48
- await clivly.sync(); // force a delta sync now
60
+
61
+ // Optional: let Clivly's dashboard start a sync in this app.
62
+ const remote = createClivlySDK({
63
+ apiKey: process.env.CLIVLY_API_KEY!,
64
+ entities,
65
+ source: { /* … */ },
66
+ syncTrigger: {
67
+ url: "https://yourapp.com/api/clivly/tick",
68
+ secret: process.env.CLIVLY_TRIGGER_SECRET, // whsec_… from Integrations → Remote sync
69
+ },
70
+ });
71
+
72
+ export async function POST(request: Request) {
73
+ return remote.createClivlyHandler()(request);
74
+ }
49
75
  ```
50
76
 
51
- `fetchPage` returns rows keyed by your source column names. Clivly applies the
52
- field map you confirmed during onboarding, so re-mapping never needs a redeploy.
77
+ `fromDrizzle` generates the keyset-paginated `fetchPage` the SDK needs, inferring the
78
+ tie-breaker id from the table's primary key pass `idField` to override.
79
+ `cursorField` and `idField` are Drizzle property keys (`updatedAt`), not database
80
+ column names (`updated_at`). Rows are keyed by your source column names; Clivly
81
+ applies the field map you confirmed during onboarding, so re-mapping never needs
82
+ a redeploy. Non-Drizzle hosts can still pass a hand-written `fetchPage` closure
83
+ instead.
84
+
85
+ With `syncTrigger.url` + `syncTrigger.secret` configured, the SDK reports the
86
+ trigger URL on each heartbeat and rejects unsigned or badly-signed trigger
87
+ requests. Without `syncTrigger`, cloud mode is push-only: Clivly can observe
88
+ the app but cannot start a sync from the dashboard.
89
+
90
+ If you rotate a remote-sync secret from Clivly's dashboard after the encryption
91
+ rollout, Clivly stores it encrypted at rest. The server-side deployment must
92
+ have `CLIVLY_TRIGGER_SECRET_WRAPPING_KEY` configured, while your app continues
93
+ to use `CLIVLY_TRIGGER_SECRET` unchanged. Existing plaintext rows remain valid
94
+ during rollout, and older rows can be backfilled with
95
+ `node ./scripts/backfill-sync-trigger-secrets.mjs --write`.
96
+
97
+ ## Vite / TanStack Start
98
+
99
+ For long-lived Vite dev servers, wire the SDK lifecycle through the Vite plugin:
100
+
101
+ ```ts
102
+ import { clivlyVite } from "@clivly/sdk/vite";
103
+ import clivly from "./clivly.config";
104
+ import { defineConfig } from "vite";
105
+
106
+ export default defineConfig({
107
+ plugins: [clivlyVite(clivly)],
108
+ });
109
+ ```
110
+
111
+ The plugin calls `connect()` for a clear startup status line, then `start()` for
112
+ the heartbeat and sync loop. It is dev-server only; use `runScheduled()` or
113
+ `createClivlyHandler()` for serverless production runtimes.
114
+
115
+ > **Legacy:** the flat `contactEntity` / `contactFields` form is still accepted but
116
+ > **deprecated** — prefer the `entities` config above, which is the same model the
117
+ > mapping table and sync engine share.
118
+
119
+ ## Verify your setup
120
+
121
+ Put your instance in `clivly.config.ts` (`export default createClivlySDK({...})`), then:
122
+
123
+ ```bash
124
+ npx clivly ping # CLIVLY_API_KEY + API reachability
125
+ npx clivly doctor # env → config → schema → columns → sample fetch → heartbeat
126
+ ```
127
+
128
+ In a monorepo, pass the app config explicitly:
129
+
130
+ ```bash
131
+ npx clivly doctor --config ./apps/web/clivly.config.ts
132
+ ```
133
+
134
+ Or in code: `await clivly.doctor()` returns `{ ok, checks }` — the same six read-only checks.
135
+
136
+ ## Serverless / edge
137
+
138
+ On request-scoped runtimes (Cloudflare Workers, Vercel, Lambda) there is no long-lived
139
+ process, so `start()`'s `setInterval` keep-alive loop can't persist — it stops ticking after
140
+ the first response. **Don't call `start()` there.** Instead drive one-shot runs from a
141
+ scheduled trigger with `runScheduled()`, or mount `createClivlyHandler()` at a route:
142
+
143
+ ```ts
144
+ // Cloudflare Workers
145
+ export default {
146
+ scheduled: () => clivly.runScheduled(), // cron: one heartbeat + one sync
147
+ fetch: clivly.createClivlyHandler(), // or drive via an uptime ping / external scheduler
148
+ };
149
+ ```
150
+
151
+ `runScheduled()` throws if a sync push fails (so a cron run is marked failed);
152
+ `createClivlyHandler()` returns `200` when the triggered run finishes successfully,
153
+ `500` otherwise. If the run carries a Clivly-issued run id, the SDK also reports
154
+ the final outcome back to Clivly so the dashboard reflects `running` vs
155
+ `succeeded` vs `failed` truthfully. `start()` emits a warning if it detects a
156
+ serverless runtime. Verify a connection from the terminal any time with
157
+ `npx clivly ping`.
158
+
159
+ ### Persisting the delta cursor
160
+
161
+ Delta sync tracks a per-entity cursor so each run only pages rows changed since the last one.
162
+ By default that cursor lives in memory — fine for a long-lived process, but **lost on every
163
+ serverless cold start**, so a fresh isolate would re-page the whole entity. Pass a
164
+ `cursorStore` backed by your own KV/DB to persist it:
165
+
166
+ ```ts
167
+ const clivly = createClivlySDK({
168
+ apiKey: process.env.CLIVLY_API_KEY!,
169
+ entities,
170
+ source: { /* … */ },
171
+ cursorStore: {
172
+ // `SyncCursor` is { id: string; time: Date } — serialize `time` (e.g. ISO string).
173
+ get: async (entity) => {
174
+ const raw = await kv.get(`clivly:cursor:${entity}`);
175
+ return raw ? { id: raw.id, time: new Date(raw.time) } : null;
176
+ },
177
+ set: (entity, cursor) =>
178
+ kv.put(`clivly:cursor:${entity}`, cursor && { id: cursor.id, time: cursor.time.toISOString() }),
179
+ },
180
+ });
181
+ ```
182
+
183
+ Delta sync is **at-least-once**: after a cold start with no persisted cursor, rows since the
184
+ last push may be re-sent (the server dedupes by id) — it never silently skips rows. Use
185
+ `sync({ mode: "full" })` when you need the server to reconcile deletions (archive-on-leave).
186
+
187
+ ## Many-to-many company membership
188
+
189
+ If a mapping links contacts to companies through a join table
190
+ (`via: { through: … }`), that entity's source must return **one row per
191
+ membership**, each row carrying the join table's foreign-key column.
192
+
193
+ ```ts
194
+ // Ada belongs to two organizations: two rows, differing only in org_id.
195
+ { id: "u1", full_name: "Ada", email: "ada@acme.test", org_id: "acme" }
196
+ { id: "u1", full_name: "Ada", email: "ada@acme.test", org_id: "globex" }
197
+ ```
198
+
199
+ This mirrors what the embedded model produces with `LEFT JOIN <join table>`, and
200
+ it is what lets Clivly keep one CRM row per membership instead of collapsing the
201
+ person onto whichever company happened to come first.
202
+
203
+ A person with no membership may omit the column entirely or send it as `null` —
204
+ both are accepted. But if **no** pushed row carries the column, the push is
205
+ rejected with `400 invalid_config`: silently keeping one membership would be
206
+ worse than failing, because the row counts look correct either way.
207
+
208
+ ## Known limitations
209
+
210
+ - `start()` is for long-lived runtimes only. On serverless/edge, use
211
+ `runScheduled()` or `createClivlyHandler()`.
212
+ - `fromDrizzle` is the first-class generated source path today. Non-Drizzle hosts
213
+ should provide a hand-written `fetchPage`.
214
+ - The flat `contactEntity` / `contactFields` config still works but is deprecated
215
+ in favor of `defineClivlyConfig({ entities: ... })`.
216
+
217
+ ## More docs
218
+
219
+ - Onboarding guide:
220
+ [`docs/guides/connecting-to-clivly-cloud.md`](../../docs/guides/connecting-to-clivly-cloud.md)
221
+ - CLI docs: [`../cli/README.md`](../cli/README.md)
222
+ - Release notes: [`../../CHANGELOG.md`](../../CHANGELOG.md)
@@ -0,0 +1,4 @@
1
+ //#region src/drizzle-meta.ts
2
+ const DRIZZLE_META = "__clivly_drizzle_source_meta__";
3
+ //#endregion
4
+ export { DRIZZLE_META as t };
@@ -0,0 +1,9 @@
1
+ //#region src/drizzle-meta.ts
2
+ const DRIZZLE_META = "__clivly_drizzle_source_meta__";
3
+ //#endregion
4
+ Object.defineProperty(exports, "DRIZZLE_META", {
5
+ enumerable: true,
6
+ get: function() {
7
+ return DRIZZLE_META;
8
+ }
9
+ });
@@ -0,0 +1,107 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_drizzle_meta = require("./drizzle-meta-iAu8w1hj.cjs");
3
+ let drizzle_orm = require("drizzle-orm");
4
+ let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
5
+ //#region src/drizzle.ts
6
+ function buildColumnsMeta(table) {
7
+ const columns = (0, drizzle_orm.getTableColumns)(table);
8
+ const fkByColumn = /* @__PURE__ */ new Map();
9
+ try {
10
+ for (const fk of (0, drizzle_orm_pg_core.getTableConfig)(table).foreignKeys) {
11
+ const ref = fk.reference();
12
+ const foreignTable = (0, drizzle_orm.getTableName)(ref.foreignTable);
13
+ ref.columns.forEach((col, i) => {
14
+ const foreignCol = ref.foreignColumns[i];
15
+ if (foreignCol) fkByColumn.set(col.name, {
16
+ table: foreignTable,
17
+ column: foreignCol.name
18
+ });
19
+ });
20
+ }
21
+ } catch {}
22
+ return Object.values(columns).map((col) => {
23
+ const references = fkByColumn.get(col.name);
24
+ let type;
25
+ try {
26
+ type = col.getSQLType();
27
+ } catch {
28
+ type = void 0;
29
+ }
30
+ return {
31
+ name: col.name,
32
+ type,
33
+ nullable: !col.notNull,
34
+ isPrimaryKey: Boolean(col.primary),
35
+ isForeignKey: references !== void 0,
36
+ ...references ? { references } : {}
37
+ };
38
+ });
39
+ }
40
+ function inferIdField(table) {
41
+ const columns = (0, drizzle_orm.getTableColumns)(table);
42
+ const primaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
43
+ if (primaryKeys.length === 1) {
44
+ const primaryKeyField = primaryKeys[0]?.[0];
45
+ if (!primaryKeyField) throw new Error(`fromDrizzle: unable to extract primary key field name from table "${(0, drizzle_orm.getTableName)(table)}".`);
46
+ return primaryKeyField;
47
+ }
48
+ const kind = primaryKeys.length === 0 ? "no" : "a composite";
49
+ throw new Error(`fromDrizzle: table "${(0, drizzle_orm.getTableName)(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
50
+ }
51
+ /**
52
+ * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
53
+ * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
54
+ * `idField` defaults to the table's single-column primary key.
55
+ *
56
+ * `drizzle-orm` is an optional peer dependency — installed only when you import
57
+ * `@clivly/sdk/drizzle`.
58
+ */
59
+ function fromDrizzle(db, table, opts) {
60
+ const drizzleDb = db;
61
+ const idField = opts.idField ?? inferIdField(table);
62
+ const columns = (0, drizzle_orm.getTableColumns)(table);
63
+ const cursorColumn = columns[opts.cursorField];
64
+ const idColumn = columns[idField];
65
+ if (!cursorColumn) throw new Error(`fromDrizzle: cursorField "${opts.cursorField}" is not a column on "${(0, drizzle_orm.getTableName)(table)}".`);
66
+ if (!idColumn) throw new Error(`fromDrizzle: idField "${idField}" is not a column on "${(0, drizzle_orm.getTableName)(table)}".`);
67
+ const source = {
68
+ entity: opts.entity,
69
+ cursorField: opts.cursorField,
70
+ idField,
71
+ 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)
72
+ };
73
+ const meta = {
74
+ columnKeys: Object.keys(columns),
75
+ cursorField: opts.cursorField,
76
+ idField,
77
+ tableName: (0, drizzle_orm.getTableName)(table),
78
+ columnsMeta: buildColumnsMeta(table)
79
+ };
80
+ Object.defineProperty(source, require_drizzle_meta.DRIZZLE_META, {
81
+ value: meta,
82
+ enumerable: false
83
+ });
84
+ return source;
85
+ }
86
+ /**
87
+ * Build a `NamespaceIntrospector` from any Drizzle client that exposes
88
+ * `.execute()`. Reads `information_schema` in the connection's default schema
89
+ * (`current_schema()`), so it reports exactly the tables the app can reach.
90
+ */
91
+ function drizzleIntrospector(db) {
92
+ const client = db;
93
+ return {
94
+ async listTables() {
95
+ return (await client.execute(drizzle_orm.sql`
96
+ select table_name from information_schema.tables
97
+ where table_schema = current_schema()
98
+ `)).rows.map((r) => String(r.table_name));
99
+ },
100
+ async sampleSelect(table) {
101
+ await client.execute(drizzle_orm.sql`select 1 from ${drizzle_orm.sql.identifier(table)} limit 1`);
102
+ }
103
+ };
104
+ }
105
+ //#endregion
106
+ exports.drizzleIntrospector = drizzleIntrospector;
107
+ exports.fromDrizzle = fromDrizzle;
@@ -0,0 +1,28 @@
1
+ import { d as SyncSource, t as NamespaceIntrospector } from "./namespace-probe-DIBgCQca.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
+ /**
22
+ * Build a `NamespaceIntrospector` from any Drizzle client that exposes
23
+ * `.execute()`. Reads `information_schema` in the connection's default schema
24
+ * (`current_schema()`), so it reports exactly the tables the app can reach.
25
+ */
26
+ declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
27
+ //#endregion
28
+ export { drizzleIntrospector, fromDrizzle };
@@ -0,0 +1,28 @@
1
+ import { d as SyncSource, t as NamespaceIntrospector } from "./namespace-probe-DFpvfl7T.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
+ /**
22
+ * Build a `NamespaceIntrospector` from any Drizzle client that exposes
23
+ * `.execute()`. Reads `information_schema` in the connection's default schema
24
+ * (`current_schema()`), so it reports exactly the tables the app can reach.
25
+ */
26
+ declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
27
+ //#endregion
28
+ export { drizzleIntrospector, fromDrizzle };
@@ -0,0 +1,105 @@
1
+ import { t as DRIZZLE_META } from "./drizzle-meta-DMx_9nlj.mjs";
2
+ import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
3
+ import { getTableConfig } from "drizzle-orm/pg-core";
4
+ //#region src/drizzle.ts
5
+ function buildColumnsMeta(table) {
6
+ const columns = getTableColumns(table);
7
+ const fkByColumn = /* @__PURE__ */ new Map();
8
+ try {
9
+ for (const fk of getTableConfig(table).foreignKeys) {
10
+ const ref = fk.reference();
11
+ const foreignTable = getTableName(ref.foreignTable);
12
+ ref.columns.forEach((col, i) => {
13
+ const foreignCol = ref.foreignColumns[i];
14
+ if (foreignCol) fkByColumn.set(col.name, {
15
+ table: foreignTable,
16
+ column: foreignCol.name
17
+ });
18
+ });
19
+ }
20
+ } catch {}
21
+ return Object.values(columns).map((col) => {
22
+ const references = fkByColumn.get(col.name);
23
+ let type;
24
+ try {
25
+ type = col.getSQLType();
26
+ } catch {
27
+ type = void 0;
28
+ }
29
+ return {
30
+ name: col.name,
31
+ type,
32
+ nullable: !col.notNull,
33
+ isPrimaryKey: Boolean(col.primary),
34
+ isForeignKey: references !== void 0,
35
+ ...references ? { references } : {}
36
+ };
37
+ });
38
+ }
39
+ function inferIdField(table) {
40
+ const columns = getTableColumns(table);
41
+ const primaryKeys = Object.entries(columns).filter(([, column]) => column.primary);
42
+ if (primaryKeys.length === 1) {
43
+ const primaryKeyField = primaryKeys[0]?.[0];
44
+ if (!primaryKeyField) throw new Error(`fromDrizzle: unable to extract primary key field name from table "${getTableName(table)}".`);
45
+ return primaryKeyField;
46
+ }
47
+ const kind = primaryKeys.length === 0 ? "no" : "a composite";
48
+ throw new Error(`fromDrizzle: table "${getTableName(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
49
+ }
50
+ /**
51
+ * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
52
+ * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
53
+ * `idField` defaults to the table's single-column primary key.
54
+ *
55
+ * `drizzle-orm` is an optional peer dependency — installed only when you import
56
+ * `@clivly/sdk/drizzle`.
57
+ */
58
+ function fromDrizzle(db, table, opts) {
59
+ const drizzleDb = db;
60
+ const idField = opts.idField ?? inferIdField(table);
61
+ const columns = getTableColumns(table);
62
+ const cursorColumn = columns[opts.cursorField];
63
+ const idColumn = columns[idField];
64
+ if (!cursorColumn) throw new Error(`fromDrizzle: cursorField "${opts.cursorField}" is not a column on "${getTableName(table)}".`);
65
+ if (!idColumn) throw new Error(`fromDrizzle: idField "${idField}" is not a column on "${getTableName(table)}".`);
66
+ const source = {
67
+ entity: opts.entity,
68
+ cursorField: opts.cursorField,
69
+ idField,
70
+ 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)
71
+ };
72
+ const meta = {
73
+ columnKeys: Object.keys(columns),
74
+ cursorField: opts.cursorField,
75
+ idField,
76
+ tableName: getTableName(table),
77
+ columnsMeta: buildColumnsMeta(table)
78
+ };
79
+ Object.defineProperty(source, DRIZZLE_META, {
80
+ value: meta,
81
+ enumerable: false
82
+ });
83
+ return source;
84
+ }
85
+ /**
86
+ * Build a `NamespaceIntrospector` from any Drizzle client that exposes
87
+ * `.execute()`. Reads `information_schema` in the connection's default schema
88
+ * (`current_schema()`), so it reports exactly the tables the app can reach.
89
+ */
90
+ function drizzleIntrospector(db) {
91
+ const client = db;
92
+ return {
93
+ async listTables() {
94
+ return (await client.execute(sql`
95
+ select table_name from information_schema.tables
96
+ where table_schema = current_schema()
97
+ `)).rows.map((r) => String(r.table_name));
98
+ },
99
+ async sampleSelect(table) {
100
+ await client.execute(sql`select 1 from ${sql.identifier(table)} limit 1`);
101
+ }
102
+ };
103
+ }
104
+ //#endregion
105
+ export { drizzleIntrospector, fromDrizzle };