@clivly/core 0.8.0 → 0.9.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 +182 -16
- package/dist/drizzle.cjs +82 -1
- package/dist/drizzle.d.cts +23 -2
- package/dist/drizzle.d.mts +23 -2
- package/dist/drizzle.mjs +81 -2
- package/dist/entity-config-8Fgr6IOH.mjs +666 -0
- package/dist/entity-config-CLxI8dVb.d.cts +556 -0
- package/dist/entity-config-CLxI8dVb.d.mts +556 -0
- package/dist/entity-config-Dh8aEcgj.cjs +779 -0
- package/dist/entity-config.cjs +13 -349
- package/dist/entity-config.d.cts +2 -295
- package/dist/entity-config.d.mts +2 -295
- package/dist/entity-config.mjs +2 -339
- package/dist/entity-heuristics.cjs +28 -8
- package/dist/entity-heuristics.d.cts +2 -12
- package/dist/entity-heuristics.d.mts +2 -12
- package/dist/entity-heuristics.mjs +28 -8
- package/dist/env-file.cjs +59 -0
- package/dist/env-file.d.cts +23 -0
- package/dist/env-file.d.mts +23 -0
- package/dist/env-file.mjs +55 -0
- package/dist/index.cjs +83 -2
- package/dist/index.d.cts +72 -2
- package/dist/index.d.mts +72 -2
- package/dist/index.mjs +69 -3
- package/dist/integration-state.cjs +242 -0
- package/dist/integration-state.d.cts +88 -0
- package/dist/integration-state.d.mts +88 -0
- package/dist/integration-state.mjs +232 -0
- package/dist/mapping-form.d.cts +1 -1
- package/dist/mapping-form.d.mts +1 -1
- package/dist/mappings-config.cjs +32 -6
- package/dist/mappings-config.d.cts +7 -1
- package/dist/mappings-config.d.mts +7 -1
- package/dist/mappings-config.mjs +32 -6
- package/dist/projection-BXBkh7qp.cjs +98 -0
- package/dist/projection-C_EMRmZ8.mjs +75 -0
- package/dist/sync-engine.cjs +9 -2
- package/dist/sync-engine.d.cts +1 -1
- package/dist/sync-engine.d.mts +1 -1
- package/dist/sync-engine.mjs +9 -2
- package/dist/view-compiler-BpNI8Ttw.cjs +299 -0
- package/dist/view-compiler-CcqsGxv4.mjs +270 -0
- package/dist/view-compiler.cjs +6 -188
- package/dist/view-compiler.d.cts +12 -2
- package/dist/view-compiler.d.mts +12 -2
- package/dist/view-compiler.mjs +2 -185
- package/package.json +21 -1
package/README.md
CHANGED
|
@@ -1,34 +1,114 @@
|
|
|
1
1
|
# @clivly/core
|
|
2
2
|
|
|
3
|
-
ORM- and auth-agnostic contracts and shared domain types for
|
|
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
|
|
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
|
-
|
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, projections, 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
|
|
45
|
+
| `@clivly/core/drizzle` | `discoverFromDrizzle(schema)` — derive the discovery shape (including keys, types, nullability and FK edges) from a Drizzle schema object; plus `buildUniqueConstraints` / `buildColumnsMeta` for reflecting a single table. 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
|
-
|
|
57
|
+
### Schema discovery (Drizzle)
|
|
18
58
|
|
|
19
|
-
`discoverFromDrizzle` turns your Drizzle schema into Clivly's discovery shape,
|
|
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";
|
|
23
64
|
import * as schema from "./db/schema";
|
|
24
65
|
|
|
25
66
|
const tables = discoverFromDrizzle(schema);
|
|
26
|
-
// → [
|
|
67
|
+
// → [
|
|
68
|
+
// {
|
|
69
|
+
// name: "participants",
|
|
70
|
+
// columns: ["id", "full_name", "email", …],
|
|
71
|
+
// columnsMeta: [
|
|
72
|
+
// { name: "id", type: "uuid", nullable: false, isPrimaryKey: true, isForeignKey: false },
|
|
73
|
+
// { name: "org_id", type: "uuid", nullable: false, isPrimaryKey: false,
|
|
74
|
+
// isForeignKey: true, references: { table: "organizations", column: "id" } },
|
|
75
|
+
// …
|
|
76
|
+
// ],
|
|
77
|
+
// uniqueConstraints: [["id"], ["email"]],
|
|
78
|
+
// },
|
|
79
|
+
// …
|
|
80
|
+
// ]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Column names are the **real DB names** (`full_name`), not the Drizzle property
|
|
84
|
+
keys (`fullName`).
|
|
85
|
+
|
|
86
|
+
`columnsMeta` and `uniqueConstraints` are what [derived
|
|
87
|
+
objects](#derived-objects-projections) validate against — without them a
|
|
88
|
+
projection cannot be authored, so keep this wired up rather than hand-writing
|
|
89
|
+
`{ name, columns }` pairs.
|
|
90
|
+
|
|
91
|
+
### Defining an entity config
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { defineClivlyConfig } from "@clivly/core/entity-config";
|
|
95
|
+
|
|
96
|
+
const entities = defineClivlyConfig({
|
|
97
|
+
entities: {
|
|
98
|
+
participants: {
|
|
99
|
+
concept: "contact",
|
|
100
|
+
source: "participants", // the host table name
|
|
101
|
+
fields: { name: "full_name", email: "email" }, // canonical key → source column
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
27
105
|
```
|
|
28
106
|
|
|
29
|
-
|
|
107
|
+
`defineClivlyConfig` validates at load and warns on any non-canonical field key
|
|
108
|
+
that would mirror as empty. `CANONICAL_FIELDS` lists the accepted keys per
|
|
109
|
+
concept; `CRM_CONCEPTS` lists the concepts.
|
|
30
110
|
|
|
31
|
-
|
|
111
|
+
### Implementing an adapter
|
|
32
112
|
|
|
33
113
|
```ts
|
|
34
114
|
import type { CRMAdapter } from "@clivly/core";
|
|
@@ -39,9 +119,78 @@ function useAdapter(crm: CRMAdapter) {
|
|
|
39
119
|
}
|
|
40
120
|
```
|
|
41
121
|
|
|
42
|
-
##
|
|
122
|
+
## Derived objects (projections)
|
|
43
123
|
|
|
44
|
-
|
|
124
|
+
An ordinary entity maps one table. A **projection** derives a custom object from
|
|
125
|
+
a base table plus explicit joins, so an object that spans tables — an
|
|
126
|
+
`Enrollment` built from `enrollments`, `students` and `courses` — can be
|
|
127
|
+
expressed at all.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { validateProjection, type ProjectionMapping } from "@clivly/core";
|
|
131
|
+
|
|
132
|
+
const enrollment: ProjectionMapping = {
|
|
133
|
+
version: 1,
|
|
134
|
+
kind: "projection",
|
|
135
|
+
entityKey: "enrollment",
|
|
136
|
+
baseTable: "enrollments",
|
|
137
|
+
joins: [
|
|
138
|
+
{
|
|
139
|
+
key: "stu",
|
|
140
|
+
table: "students",
|
|
141
|
+
type: "left",
|
|
142
|
+
on: [{ left: { table: "stu", column: "id" }, op: "eq",
|
|
143
|
+
right: { table: "base", column: "student_id" } }],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
fieldBindings: {
|
|
147
|
+
student_name: { from: { table: "stu", column: "full_name" } },
|
|
148
|
+
term: { from: { table: "base", column: "term" } },
|
|
149
|
+
},
|
|
150
|
+
identity: { kind: "base_pk" },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const errors = validateProjection(enrollment, tables, "projection");
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Refer to the base table as `base` and each join by its `key`. Validation is
|
|
157
|
+
**fail-closed**: a table whose discovery payload lacks `columnsMeta` /
|
|
158
|
+
`uniqueConstraints` is rejected rather than assumed safe, because identity
|
|
159
|
+
uniqueness cannot be proven without them.
|
|
160
|
+
|
|
161
|
+
### Fan-out
|
|
162
|
+
|
|
163
|
+
A join that does not cover a unique key of the joined table can match many rows
|
|
164
|
+
per base row. That is allowed — but the base table's primary key is then no
|
|
165
|
+
longer unique per output row, so validation enforces: at most one fan-out join,
|
|
166
|
+
no `base_pk` identity on a fanning join, and an identity that includes a unique
|
|
167
|
+
key of the fanning table. `fanOutJoins` reports which joins fan out.
|
|
168
|
+
|
|
169
|
+
Composite identities concatenate their parts with `::`, so every part must be
|
|
170
|
+
`NOT NULL` (`CONCAT_WS` skips nulls) and non-text (a value containing `::` could
|
|
171
|
+
collide).
|
|
172
|
+
|
|
173
|
+
### Building one interactively
|
|
174
|
+
|
|
175
|
+
`ProjectionDraft` and its helpers exist so a builder UI stays presentational —
|
|
176
|
+
every rule is a pure function of draft + schema:
|
|
177
|
+
|
|
178
|
+
| Helper | Purpose |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `emptyProjectionDraft(baseTable)` | A blank draft |
|
|
181
|
+
| `suggestJoinConditions(base, joinTable, schema, key)` | Pre-fill a join's `ON` from foreign-key metadata |
|
|
182
|
+
| `draftToProjection(draft)` | `ProjectionMapping`, or `null` while incomplete |
|
|
183
|
+
| `draftErrors(draft, schema)` | Validation errors for the current draft |
|
|
184
|
+
| `describeCardinality(draft, schema)` | Per-join `{ joinKey, fansOut }` for inline hints |
|
|
185
|
+
|
|
186
|
+
Full guide, including how to choose an identity and the current limits:
|
|
187
|
+
[`docs/guides/derived-objects.md`](https://github.com/amani-joseph/clivly.com/blob/master/docs/guides/derived-objects.md).
|
|
188
|
+
|
|
189
|
+
## Advanced: the `$raw` escape hatch
|
|
190
|
+
|
|
191
|
+
Entity `filter`s and relationship `via`s accept a `$raw` form — an opaque SQL
|
|
192
|
+
fragment for predicates the operators can't express (e.g. a cross-table
|
|
193
|
+
condition):
|
|
45
194
|
|
|
46
195
|
```ts
|
|
47
196
|
defineClivlyConfig({
|
|
@@ -56,14 +205,21 @@ defineClivlyConfig({
|
|
|
56
205
|
});
|
|
57
206
|
```
|
|
58
207
|
|
|
59
|
-
**`$raw` is unchecked.** `validateEntitiesConfig` deliberately **skips** `$raw`
|
|
208
|
+
**`$raw` is unchecked.** `validateEntitiesConfig` deliberately **skips** `$raw`
|
|
209
|
+
filters and joins — it can't know your columns are real or your SQL is valid.
|
|
210
|
+
Two consequences:
|
|
60
211
|
|
|
61
|
-
- **Injection safety is on you.** Never interpolate untrusted input into a
|
|
212
|
+
- **Injection safety is on you.** Never interpolate untrusted input into a
|
|
213
|
+
`$raw` string; treat it like handwritten SQL. Everything else in the config is
|
|
214
|
+
escaped for you; `$raw` is not.
|
|
62
215
|
- **Mistakes surface at sync time, not config time** — unless you dry-run it.
|
|
63
216
|
|
|
64
217
|
### Dry-run with `explainEntitiesConfig`
|
|
65
218
|
|
|
66
|
-
`explainEntitiesConfig(config, run, options?)` compiles each entity's view and
|
|
219
|
+
`explainEntitiesConfig(config, run, options?)` compiles each entity's view and
|
|
220
|
+
executes it via a runner you provide, so a broken `$raw` fragment fails
|
|
221
|
+
**before** the first sync. It never throws — it returns one `ExplainResult` per
|
|
222
|
+
entity:
|
|
67
223
|
|
|
68
224
|
```ts
|
|
69
225
|
import { explainEntitiesConfig } from "@clivly/core";
|
|
@@ -81,6 +237,16 @@ for (const r of results) {
|
|
|
81
237
|
}
|
|
82
238
|
```
|
|
83
239
|
|
|
240
|
+
`clivly status` runs the same mapping dry-run as one of its setup checks, so you
|
|
241
|
+
get this during normal verification without wiring it up yourself.
|
|
242
|
+
|
|
243
|
+
## Related documentation
|
|
244
|
+
|
|
245
|
+
- **[Connecting to Clivly Cloud](../../docs/guides/connecting-to-clivly-cloud.md)** — the canonical end-to-end guide
|
|
246
|
+
- [`@clivly/sdk` README](../sdk/README.md) — the runtime that consumes this config
|
|
247
|
+
- [`clivly` CLI README](../cli/README.md) — every command and flag
|
|
248
|
+
- [`STABILITY.md`](../../STABILITY.md) — import tiers before 1.0
|
|
249
|
+
|
|
84
250
|
## License
|
|
85
251
|
|
|
86
252
|
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;
|
package/dist/drizzle.d.cts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
import { DiscoveredSchemaTable } from "./entity-config.cjs";
|
|
1
|
+
import { f as DiscoveredColumnMeta, p as DiscoveredSchemaTable } from "./entity-config-CLxI8dVb.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 };
|
package/dist/drizzle.d.mts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
import { DiscoveredSchemaTable } from "./entity-config.mjs";
|
|
1
|
+
import { f as DiscoveredColumnMeta, p as DiscoveredSchemaTable } from "./entity-config-CLxI8dVb.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 };
|