@clivly/core 0.8.0 → 0.9.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.
Files changed (42) hide show
  1. package/README.md +96 -15
  2. package/dist/drizzle.cjs +82 -1
  3. package/dist/drizzle.d.cts +23 -2
  4. package/dist/drizzle.d.mts +23 -2
  5. package/dist/drizzle.mjs +81 -2
  6. package/dist/entity-config-PoOGzvpN.cjs +728 -0
  7. package/dist/entity-config-Tb5bbKLj.mjs +621 -0
  8. package/dist/entity-config.cjs +12 -349
  9. package/dist/entity-config.d.cts +120 -1
  10. package/dist/entity-config.d.mts +120 -1
  11. package/dist/entity-config.mjs +1 -338
  12. package/dist/entity-heuristics.cjs +28 -8
  13. package/dist/entity-heuristics.d.cts +2 -12
  14. package/dist/entity-heuristics.d.mts +2 -12
  15. package/dist/entity-heuristics.mjs +28 -8
  16. package/dist/env-file.cjs +59 -0
  17. package/dist/env-file.d.cts +23 -0
  18. package/dist/env-file.d.mts +23 -0
  19. package/dist/env-file.mjs +55 -0
  20. package/dist/index.cjs +83 -2
  21. package/dist/index.d.cts +73 -3
  22. package/dist/index.d.mts +73 -3
  23. package/dist/index.mjs +69 -3
  24. package/dist/integration-state.cjs +242 -0
  25. package/dist/integration-state.d.cts +88 -0
  26. package/dist/integration-state.d.mts +88 -0
  27. package/dist/integration-state.mjs +232 -0
  28. package/dist/mappings-config-B0W5bVZj.d.mts +168 -0
  29. package/dist/mappings-config-BWo4cRDF.d.cts +168 -0
  30. package/dist/mappings-config.cjs +32 -6
  31. package/dist/mappings-config.d.cts +1 -40
  32. package/dist/mappings-config.d.mts +1 -40
  33. package/dist/mappings-config.mjs +32 -6
  34. package/dist/projection-BXBkh7qp.cjs +98 -0
  35. package/dist/projection-C_EMRmZ8.mjs +75 -0
  36. package/dist/sync-engine.cjs +9 -2
  37. package/dist/sync-engine.mjs +9 -2
  38. package/dist/view-compiler-BKrBA_II.mjs +254 -0
  39. package/dist/view-compiler-BwZ9UgLp.cjs +277 -0
  40. package/dist/view-compiler.cjs +5 -188
  41. package/dist/view-compiler.mjs +1 -184
  42. package/package.json +21 -1
package/README.md CHANGED
@@ -1,22 +1,63 @@
1
1
  # @clivly/core
2
2
 
3
- ORM- and auth-agnostic contracts and shared domain types for [Clivly](https://clivly.com) — the CRM that lives inside your app.
3
+ ORM- and auth-agnostic contracts and shared domain types for
4
+ [Clivly](https://clivly.com) — the CRM that lives inside your app.
4
5
 
5
- This package defines the interfaces that ORM and auth adapters implement. It has no runtime dependencies.
6
+ This package defines the interfaces that ORM and auth adapters implement, the
7
+ entity-config schema shared by the SDK and the dashboard's mapping table, and
8
+ the view compiler that turns a mapping into SQL. Its only runtime dependency is
9
+ `zod`.
10
+
11
+ > **Most integrators never install this directly.** `defineClivlyConfig` is
12
+ > re-exported from `clivly/sdk`, which is where the
13
+ > [integration guide](../../docs/guides/connecting-to-clivly-cloud.md) tells you
14
+ > to import it from. Reach for `@clivly/core` when you are building an adapter
15
+ > or working with the entity config outside an SDK instance.
16
+
17
+ ## Prerequisites
18
+
19
+ | Requirement | Version / note |
20
+ | --- | --- |
21
+ | Node.js | **≥ 22** |
22
+ | `drizzle-orm` | **Optional peer** — only needed for the `/drizzle` subpath |
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @clivly/core
28
+ ```
6
29
 
7
30
  ## Exports
8
31
 
32
+ Every subpath below is importable independently, so you pull in only what you
33
+ use. The root (`.`) re-exports the contracts, entity config, mapping form, sync
34
+ engine and view compiler — but **not** `/drizzle` (which needs the optional
35
+ peer), `/entity-heuristics`, `/mapping-score`, `/integration-state` or
36
+ `/env-file`.
37
+
9
38
  | Entry | Contents |
10
- |---|---|
11
- | `.` | Everything below, re-exported |
39
+ | --- | --- |
40
+ | `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, shared types |
12
41
  | `@clivly/core/adapter` | `CRMAdapter` — the interface every ORM implementation fulfils (org-scoped; contacts, companies, deals, tasks, activities) |
13
- | `@clivly/core/auth-adapter` | `ClivlyAuthAdapter`, `authCustom()`, `MembershipResolver` — pluggable host-app identity + the `crm_members` authorization layer |
42
+ | `@clivly/core/auth-adapter` | `ClivlyAuthAdapter`, `ClivlyUser`, `authCustom()`, `MembershipResolver`, `CrmMembership` — pluggable host-app identity + the `crm_members` authorization layer |
43
+ | `@clivly/core/entity-config` | `defineClivlyConfig`, `validateEntitiesConfig`, `entitiesConfigSchema`, `CANONICAL_FIELDS`, `CRM_CONCEPTS`, `ClivlyConfigError`, and the config types |
14
44
  | `@clivly/core/types` | Shared domain types (`Contact`, `Company`, `Deal`, `Task`, `Note`, `Activity`, inputs, filters, `CrmRole`) |
15
- | `@clivly/core/drizzle` | `discoverFromDrizzle(schema)` — derive the discovery shape from a Drizzle schema object. Requires the optional `drizzle-orm` peer dependency |
45
+ | `@clivly/core/drizzle` | `discoverFromDrizzle(schema)` — derive the discovery shape from a Drizzle schema object. Requires the optional `drizzle-orm` peer |
46
+ | `@clivly/core/view-compiler` | `compileEntityView`, `compileEntityViews`, `explainEntitiesConfig`, `DEFAULT_VIEW_PREFIX` |
47
+ | `@clivly/core/mappings-config` | `mappingsToEntitiesConfig`, `EntityMappingRow` — turn `crm_entity_mappings` rows into an entities config |
48
+ | `@clivly/core/mapping-form` | `buildFilterFromDiscriminator`, `buildRelationships`, `relationshipViaSignature` — mapping-UI helpers |
49
+ | `@clivly/core/mapping-score` | Scoring helpers for ranking candidate mappings |
50
+ | `@clivly/core/entity-heuristics` | Table-name heuristics that guess which table holds contacts/companies |
51
+ | `@clivly/core/sync-engine` | `reconcile`, `runSync`, `SyncStore` and friends — the mirror reconciliation engine |
52
+ | `@clivly/core/integration-state` | `INSTALL_COMMAND` and the shared onboarding state machine |
53
+ | `@clivly/core/env-file` | `envFileFor(framework)` — the conventional env filename per framework |
54
+
55
+ ## Five-minute quick start
16
56
 
17
- ## Schema discovery (Drizzle)
57
+ ### Schema discovery (Drizzle)
18
58
 
19
- `discoverFromDrizzle` turns your Drizzle schema into Clivly's discovery shape, so you don't hand-author table/column arrays:
59
+ `discoverFromDrizzle` turns your Drizzle schema into Clivly's discovery shape,
60
+ so you don't hand-author table/column arrays:
20
61
 
21
62
  ```ts
22
63
  import { discoverFromDrizzle } from "@clivly/core/drizzle";
@@ -26,9 +67,30 @@ const tables = discoverFromDrizzle(schema);
26
67
  // → [{ name: "participants", columns: ["id", "full_name", "email", …] }, …]
27
68
  ```
28
69
 
29
- Column names are the real DB names (`full_name`), not the Drizzle keys (`fullName`). `drizzle-orm` is an **optional peer dependency** — only needed when you import this subpath.
70
+ Column names are the **real DB names** (`full_name`), not the Drizzle property
71
+ keys (`fullName`).
72
+
73
+ ### Defining an entity config
74
+
75
+ ```ts
76
+ import { defineClivlyConfig } from "@clivly/core/entity-config";
77
+
78
+ const entities = defineClivlyConfig({
79
+ entities: {
80
+ participants: {
81
+ concept: "contact",
82
+ source: "participants", // the host table name
83
+ fields: { name: "full_name", email: "email" }, // canonical key → source column
84
+ },
85
+ },
86
+ });
87
+ ```
88
+
89
+ `defineClivlyConfig` validates at load and warns on any non-canonical field key
90
+ that would mirror as empty. `CANONICAL_FIELDS` lists the accepted keys per
91
+ concept; `CRM_CONCEPTS` lists the concepts.
30
92
 
31
- ## Usage
93
+ ### Implementing an adapter
32
94
 
33
95
  ```ts
34
96
  import type { CRMAdapter } from "@clivly/core";
@@ -39,9 +101,11 @@ function useAdapter(crm: CRMAdapter) {
39
101
  }
40
102
  ```
41
103
 
42
- ## The `$raw` escape hatch
104
+ ## Advanced: the `$raw` escape hatch
43
105
 
44
- Entity `filter`s and relationship `via`s accept a `$raw` form — an opaque SQL fragment for predicates the operators can't express (e.g. a cross-table condition):
106
+ Entity `filter`s and relationship `via`s accept a `$raw` form — an opaque SQL
107
+ fragment for predicates the operators can't express (e.g. a cross-table
108
+ condition):
45
109
 
46
110
  ```ts
47
111
  defineClivlyConfig({
@@ -56,14 +120,21 @@ defineClivlyConfig({
56
120
  });
57
121
  ```
58
122
 
59
- **`$raw` is unchecked.** `validateEntitiesConfig` deliberately **skips** `$raw` filters and joins — it can't know your columns are real or your SQL is valid. Two consequences:
123
+ **`$raw` is unchecked.** `validateEntitiesConfig` deliberately **skips** `$raw`
124
+ filters and joins — it can't know your columns are real or your SQL is valid.
125
+ Two consequences:
60
126
 
61
- - **Injection safety is on you.** Never interpolate untrusted input into a `$raw` string; treat it like handwritten SQL. Everything else in the config is escaped for you; `$raw` is not.
127
+ - **Injection safety is on you.** Never interpolate untrusted input into a
128
+ `$raw` string; treat it like handwritten SQL. Everything else in the config is
129
+ escaped for you; `$raw` is not.
62
130
  - **Mistakes surface at sync time, not config time** — unless you dry-run it.
63
131
 
64
132
  ### Dry-run with `explainEntitiesConfig`
65
133
 
66
- `explainEntitiesConfig(config, run, options?)` compiles each entity's view and executes it via a runner you provide, so a broken `$raw` fragment fails **before** the first sync. It never throws — it returns one `ExplainResult` per entity:
134
+ `explainEntitiesConfig(config, run, options?)` compiles each entity's view and
135
+ executes it via a runner you provide, so a broken `$raw` fragment fails
136
+ **before** the first sync. It never throws — it returns one `ExplainResult` per
137
+ entity:
67
138
 
68
139
  ```ts
69
140
  import { explainEntitiesConfig } from "@clivly/core";
@@ -81,6 +152,16 @@ for (const r of results) {
81
152
  }
82
153
  ```
83
154
 
155
+ `clivly status` runs the same mapping dry-run as one of its setup checks, so you
156
+ get this during normal verification without wiring it up yourself.
157
+
158
+ ## Related documentation
159
+
160
+ - **[Connecting to Clivly Cloud](../../docs/guides/connecting-to-clivly-cloud.md)** — the canonical end-to-end guide
161
+ - [`@clivly/sdk` README](../sdk/README.md) — the runtime that consumes this config
162
+ - [`clivly` CLI README](../cli/README.md) — every command and flag
163
+ - [`STABILITY.md`](../../STABILITY.md) — import tiers before 1.0
164
+
84
165
  ## License
85
166
 
86
167
  MIT
package/dist/drizzle.cjs CHANGED
@@ -1,7 +1,83 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let drizzle_orm = require("drizzle-orm");
3
+ let drizzle_orm_pg_core = require("drizzle-orm/pg-core");
3
4
  //#region src/drizzle.ts
4
5
  /**
6
+ * Every unique key on a table, as DB column-name tuples. Primary keys count —
7
+ * fan-out detection asks "do these join columns cover a unique key?", and a PK
8
+ * answers that as well as any explicit unique constraint.
9
+ *
10
+ * Returns `undefined` if reflection fails (non-pg table, or a drizzle version
11
+ * without config access), meaning "unknown, cannot determine". Returns `[]`
12
+ * when reflection succeeds and the table genuinely has none. That distinction
13
+ * is load-bearing: `hasRichMeta` uses it to tell "metadata unavailable" from
14
+ * "confirmed no constraints", and projection validation fails closed on the
15
+ * former.
16
+ */
17
+ function buildUniqueConstraints(table) {
18
+ const keys = [];
19
+ try {
20
+ const config = (0, drizzle_orm_pg_core.getTableConfig)(table);
21
+ for (const column of config.columns ?? []) if (column.isUnique || column.primary) keys.push([column.name]);
22
+ for (const pk of config.primaryKeys ?? []) keys.push(pk.columns.map((c) => c.name));
23
+ for (const constraint of config.uniqueConstraints ?? []) keys.push(constraint.columns.map((c) => c.name));
24
+ for (const index of config.indexes ?? []) {
25
+ if (!index.config?.unique) continue;
26
+ const columns = (index.config.columns ?? []).map((c) => c?.name).filter((name) => Boolean(name));
27
+ if (columns.length > 0) keys.push(columns);
28
+ }
29
+ } catch {
30
+ return;
31
+ }
32
+ const seen = /* @__PURE__ */ new Set();
33
+ return keys.filter((key) => {
34
+ const id = [...key].sort().join("\0");
35
+ if (seen.has(id) || key.length === 0) return false;
36
+ seen.add(id);
37
+ return true;
38
+ });
39
+ }
40
+ /**
41
+ * Per-column metadata (type, PK, nullability, FK edges) for a Drizzle table.
42
+ * Structural only — no DB round-trip, so it is safe to recompute on every
43
+ * heartbeat. FK reflection is best-effort; a non-Postgres table still gets
44
+ * name/type/PK/nullable.
45
+ */
46
+ function buildColumnsMeta(table) {
47
+ const columns = (0, drizzle_orm.getTableColumns)(table);
48
+ const fkByColumn = /* @__PURE__ */ new Map();
49
+ try {
50
+ for (const fk of (0, drizzle_orm_pg_core.getTableConfig)(table).foreignKeys) {
51
+ const ref = fk.reference();
52
+ const foreignTable = (0, drizzle_orm.getTableName)(ref.foreignTable);
53
+ ref.columns.forEach((col, index) => {
54
+ const foreignCol = ref.foreignColumns[index];
55
+ if (foreignCol) fkByColumn.set(col.name, {
56
+ table: foreignTable,
57
+ column: foreignCol.name
58
+ });
59
+ });
60
+ }
61
+ } catch {}
62
+ return Object.values(columns).map((col) => {
63
+ const references = fkByColumn.get(col.name);
64
+ let type;
65
+ try {
66
+ type = col.getSQLType();
67
+ } catch {
68
+ type = void 0;
69
+ }
70
+ return {
71
+ name: col.name,
72
+ type,
73
+ nullable: !col.notNull,
74
+ isPrimaryKey: Boolean(col.primary),
75
+ isForeignKey: references !== void 0,
76
+ ...references ? { references } : {}
77
+ };
78
+ });
79
+ }
80
+ /**
5
81
  * Derive Clivly's discovery shape (table + column names) from a Drizzle schema
6
82
  * object — the `* as schema` you pass to `drizzle(client, { schema })`. This
7
83
  * delivers the "reads your schema" promise for Drizzle hosts: no hand-authored
@@ -21,12 +97,17 @@ function discoverFromDrizzle(schema) {
21
97
  for (const value of Object.values(schema)) {
22
98
  if (!(0, drizzle_orm.is)(value, drizzle_orm.Table)) continue;
23
99
  const columns = (0, drizzle_orm.getTableColumns)(value);
100
+ const uniqueConstraints = buildUniqueConstraints(value);
24
101
  tables.push({
25
102
  name: (0, drizzle_orm.getTableName)(value),
26
- columns: Object.values(columns).map((column) => column.name)
103
+ columns: Object.values(columns).map((column) => column.name),
104
+ columnsMeta: buildColumnsMeta(value),
105
+ ...uniqueConstraints === void 0 ? {} : { uniqueConstraints }
27
106
  });
28
107
  }
29
108
  return tables;
30
109
  }
31
110
  //#endregion
111
+ exports.buildColumnsMeta = buildColumnsMeta;
112
+ exports.buildUniqueConstraints = buildUniqueConstraints;
32
113
  exports.discoverFromDrizzle = discoverFromDrizzle;
@@ -1,5 +1,26 @@
1
- import { DiscoveredSchemaTable } from "./entity-config.cjs";
1
+ import { DiscoveredColumnMeta, DiscoveredSchemaTable } from "./entity-config.cjs";
2
+ import { Table } from "drizzle-orm";
2
3
  //#region src/drizzle.d.ts
4
+ /**
5
+ * Every unique key on a table, as DB column-name tuples. Primary keys count —
6
+ * fan-out detection asks "do these join columns cover a unique key?", and a PK
7
+ * answers that as well as any explicit unique constraint.
8
+ *
9
+ * Returns `undefined` if reflection fails (non-pg table, or a drizzle version
10
+ * without config access), meaning "unknown, cannot determine". Returns `[]`
11
+ * when reflection succeeds and the table genuinely has none. That distinction
12
+ * is load-bearing: `hasRichMeta` uses it to tell "metadata unavailable" from
13
+ * "confirmed no constraints", and projection validation fails closed on the
14
+ * former.
15
+ */
16
+ declare function buildUniqueConstraints(table: unknown): string[][] | undefined;
17
+ /**
18
+ * Per-column metadata (type, PK, nullability, FK edges) for a Drizzle table.
19
+ * Structural only — no DB round-trip, so it is safe to recompute on every
20
+ * heartbeat. FK reflection is best-effort; a non-Postgres table still gets
21
+ * name/type/PK/nullable.
22
+ */
23
+ declare function buildColumnsMeta(table: Table): DiscoveredColumnMeta[];
3
24
  /**
4
25
  * Derive Clivly's discovery shape (table + column names) from a Drizzle schema
5
26
  * object — the `* as schema` you pass to `drizzle(client, { schema })`. This
@@ -17,4 +38,4 @@ import { DiscoveredSchemaTable } from "./entity-config.cjs";
17
38
  */
18
39
  declare function discoverFromDrizzle(schema: Record<string, unknown>): DiscoveredSchemaTable[];
19
40
  //#endregion
20
- export { discoverFromDrizzle };
41
+ export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };
@@ -1,5 +1,26 @@
1
- import { DiscoveredSchemaTable } from "./entity-config.mjs";
1
+ import { DiscoveredColumnMeta, DiscoveredSchemaTable } from "./entity-config.mjs";
2
+ import { Table } from "drizzle-orm";
2
3
  //#region src/drizzle.d.ts
4
+ /**
5
+ * Every unique key on a table, as DB column-name tuples. Primary keys count —
6
+ * fan-out detection asks "do these join columns cover a unique key?", and a PK
7
+ * answers that as well as any explicit unique constraint.
8
+ *
9
+ * Returns `undefined` if reflection fails (non-pg table, or a drizzle version
10
+ * without config access), meaning "unknown, cannot determine". Returns `[]`
11
+ * when reflection succeeds and the table genuinely has none. That distinction
12
+ * is load-bearing: `hasRichMeta` uses it to tell "metadata unavailable" from
13
+ * "confirmed no constraints", and projection validation fails closed on the
14
+ * former.
15
+ */
16
+ declare function buildUniqueConstraints(table: unknown): string[][] | undefined;
17
+ /**
18
+ * Per-column metadata (type, PK, nullability, FK edges) for a Drizzle table.
19
+ * Structural only — no DB round-trip, so it is safe to recompute on every
20
+ * heartbeat. FK reflection is best-effort; a non-Postgres table still gets
21
+ * name/type/PK/nullable.
22
+ */
23
+ declare function buildColumnsMeta(table: Table): DiscoveredColumnMeta[];
3
24
  /**
4
25
  * Derive Clivly's discovery shape (table + column names) from a Drizzle schema
5
26
  * object — the `* as schema` you pass to `drizzle(client, { schema })`. This
@@ -17,4 +38,4 @@ import { DiscoveredSchemaTable } from "./entity-config.mjs";
17
38
  */
18
39
  declare function discoverFromDrizzle(schema: Record<string, unknown>): DiscoveredSchemaTable[];
19
40
  //#endregion
20
- export { discoverFromDrizzle };
41
+ export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };
package/dist/drizzle.mjs CHANGED
@@ -1,6 +1,82 @@
1
1
  import { Table, getTableColumns, getTableName, is } from "drizzle-orm";
2
+ import { getTableConfig } from "drizzle-orm/pg-core";
2
3
  //#region src/drizzle.ts
3
4
  /**
5
+ * Every unique key on a table, as DB column-name tuples. Primary keys count —
6
+ * fan-out detection asks "do these join columns cover a unique key?", and a PK
7
+ * answers that as well as any explicit unique constraint.
8
+ *
9
+ * Returns `undefined` if reflection fails (non-pg table, or a drizzle version
10
+ * without config access), meaning "unknown, cannot determine". Returns `[]`
11
+ * when reflection succeeds and the table genuinely has none. That distinction
12
+ * is load-bearing: `hasRichMeta` uses it to tell "metadata unavailable" from
13
+ * "confirmed no constraints", and projection validation fails closed on the
14
+ * former.
15
+ */
16
+ function buildUniqueConstraints(table) {
17
+ const keys = [];
18
+ try {
19
+ const config = getTableConfig(table);
20
+ for (const column of config.columns ?? []) if (column.isUnique || column.primary) keys.push([column.name]);
21
+ for (const pk of config.primaryKeys ?? []) keys.push(pk.columns.map((c) => c.name));
22
+ for (const constraint of config.uniqueConstraints ?? []) keys.push(constraint.columns.map((c) => c.name));
23
+ for (const index of config.indexes ?? []) {
24
+ if (!index.config?.unique) continue;
25
+ const columns = (index.config.columns ?? []).map((c) => c?.name).filter((name) => Boolean(name));
26
+ if (columns.length > 0) keys.push(columns);
27
+ }
28
+ } catch {
29
+ return;
30
+ }
31
+ const seen = /* @__PURE__ */ new Set();
32
+ return keys.filter((key) => {
33
+ const id = [...key].sort().join("\0");
34
+ if (seen.has(id) || key.length === 0) return false;
35
+ seen.add(id);
36
+ return true;
37
+ });
38
+ }
39
+ /**
40
+ * Per-column metadata (type, PK, nullability, FK edges) for a Drizzle table.
41
+ * Structural only — no DB round-trip, so it is safe to recompute on every
42
+ * heartbeat. FK reflection is best-effort; a non-Postgres table still gets
43
+ * name/type/PK/nullable.
44
+ */
45
+ function buildColumnsMeta(table) {
46
+ const columns = getTableColumns(table);
47
+ const fkByColumn = /* @__PURE__ */ new Map();
48
+ try {
49
+ for (const fk of getTableConfig(table).foreignKeys) {
50
+ const ref = fk.reference();
51
+ const foreignTable = getTableName(ref.foreignTable);
52
+ ref.columns.forEach((col, index) => {
53
+ const foreignCol = ref.foreignColumns[index];
54
+ if (foreignCol) fkByColumn.set(col.name, {
55
+ table: foreignTable,
56
+ column: foreignCol.name
57
+ });
58
+ });
59
+ }
60
+ } catch {}
61
+ return Object.values(columns).map((col) => {
62
+ const references = fkByColumn.get(col.name);
63
+ let type;
64
+ try {
65
+ type = col.getSQLType();
66
+ } catch {
67
+ type = void 0;
68
+ }
69
+ return {
70
+ name: col.name,
71
+ type,
72
+ nullable: !col.notNull,
73
+ isPrimaryKey: Boolean(col.primary),
74
+ isForeignKey: references !== void 0,
75
+ ...references ? { references } : {}
76
+ };
77
+ });
78
+ }
79
+ /**
4
80
  * Derive Clivly's discovery shape (table + column names) from a Drizzle schema
5
81
  * object — the `* as schema` you pass to `drizzle(client, { schema })`. This
6
82
  * delivers the "reads your schema" promise for Drizzle hosts: no hand-authored
@@ -20,12 +96,15 @@ function discoverFromDrizzle(schema) {
20
96
  for (const value of Object.values(schema)) {
21
97
  if (!is(value, Table)) continue;
22
98
  const columns = getTableColumns(value);
99
+ const uniqueConstraints = buildUniqueConstraints(value);
23
100
  tables.push({
24
101
  name: getTableName(value),
25
- columns: Object.values(columns).map((column) => column.name)
102
+ columns: Object.values(columns).map((column) => column.name),
103
+ columnsMeta: buildColumnsMeta(value),
104
+ ...uniqueConstraints === void 0 ? {} : { uniqueConstraints }
26
105
  });
27
106
  }
28
107
  return tables;
29
108
  }
30
109
  //#endregion
31
- export { discoverFromDrizzle };
110
+ export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };