@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.
- package/README.md +96 -15
- 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-PoOGzvpN.cjs +728 -0
- package/dist/entity-config-Tb5bbKLj.mjs +621 -0
- package/dist/entity-config.cjs +12 -349
- package/dist/entity-config.d.cts +120 -1
- package/dist/entity-config.d.mts +120 -1
- package/dist/entity-config.mjs +1 -338
- 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 +73 -3
- package/dist/index.d.mts +73 -3
- 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/mappings-config-B0W5bVZj.d.mts +168 -0
- package/dist/mappings-config-BWo4cRDF.d.cts +168 -0
- package/dist/mappings-config.cjs +32 -6
- package/dist/mappings-config.d.cts +1 -40
- package/dist/mappings-config.d.mts +1 -40
- 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.mjs +9 -2
- package/dist/view-compiler-BKrBA_II.mjs +254 -0
- package/dist/view-compiler-BwZ9UgLp.cjs +277 -0
- package/dist/view-compiler.cjs +5 -188
- package/dist/view-compiler.mjs +1 -184
- 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
|
|
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, 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 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
|
-
|
|
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";
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
##
|
|
104
|
+
## Advanced: the `$raw` escape hatch
|
|
43
105
|
|
|
44
|
-
Entity `filter`s and relationship `via`s accept a `$raw` form — an opaque SQL
|
|
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`
|
|
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
|
|
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
|
|
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;
|
package/dist/drizzle.d.cts
CHANGED
|
@@ -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 };
|
package/dist/drizzle.d.mts
CHANGED
|
@@ -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 };
|