@clivly/core 0.9.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 +88 -3
- package/dist/drizzle.d.cts +1 -1
- package/dist/drizzle.d.mts +1 -1
- package/dist/{entity-config-Tb5bbKLj.mjs → entity-config-8Fgr6IOH.mjs} +47 -2
- package/dist/entity-config-CLxI8dVb.d.cts +556 -0
- package/dist/entity-config-CLxI8dVb.d.mts +556 -0
- package/dist/{entity-config-PoOGzvpN.cjs → entity-config-Dh8aEcgj.cjs} +52 -1
- package/dist/entity-config.cjs +2 -1
- package/dist/entity-config.d.cts +2 -414
- package/dist/entity-config.d.mts +2 -414
- package/dist/entity-config.mjs +2 -2
- package/dist/entity-heuristics.cjs +1 -1
- package/dist/entity-heuristics.d.cts +1 -1
- package/dist/entity-heuristics.d.mts +1 -1
- package/dist/entity-heuristics.mjs +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/mapping-form.d.cts +1 -1
- package/dist/mapping-form.d.mts +1 -1
- package/dist/mappings-config.cjs +1 -1
- package/dist/mappings-config.d.cts +46 -1
- package/dist/mappings-config.d.mts +46 -1
- package/dist/mappings-config.mjs +1 -1
- package/dist/sync-engine.cjs +1 -1
- package/dist/sync-engine.d.cts +1 -1
- package/dist/sync-engine.d.mts +1 -1
- package/dist/sync-engine.mjs +1 -1
- package/dist/{view-compiler-BwZ9UgLp.cjs → view-compiler-BpNI8Ttw.cjs} +22 -0
- package/dist/{view-compiler-BKrBA_II.mjs → view-compiler-CcqsGxv4.mjs} +17 -1
- package/dist/view-compiler.cjs +2 -1
- package/dist/view-compiler.d.cts +12 -2
- package/dist/view-compiler.d.mts +12 -2
- package/dist/view-compiler.mjs +2 -2
- package/package.json +1 -1
- package/dist/mappings-config-B0W5bVZj.d.mts +0 -168
- package/dist/mappings-config-BWo4cRDF.d.cts +0 -168
package/README.md
CHANGED
|
@@ -37,12 +37,12 @@ peer), `/entity-heuristics`, `/mapping-score`, `/integration-state` or
|
|
|
37
37
|
|
|
38
38
|
| Entry | Contents |
|
|
39
39
|
| --- | --- |
|
|
40
|
-
| `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, shared types |
|
|
40
|
+
| `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, projections, shared types |
|
|
41
41
|
| `@clivly/core/adapter` | `CRMAdapter` — the interface every ORM implementation fulfils (org-scoped; contacts, companies, deals, tasks, activities) |
|
|
42
42
|
| `@clivly/core/auth-adapter` | `ClivlyAuthAdapter`, `ClivlyUser`, `authCustom()`, `MembershipResolver`, `CrmMembership` — pluggable host-app identity + the `crm_members` authorization layer |
|
|
43
43
|
| `@clivly/core/entity-config` | `defineClivlyConfig`, `validateEntitiesConfig`, `entitiesConfigSchema`, `CANONICAL_FIELDS`, `CRM_CONCEPTS`, `ClivlyConfigError`, and the config types |
|
|
44
44
|
| `@clivly/core/types` | Shared domain types (`Contact`, `Company`, `Deal`, `Task`, `Note`, `Activity`, inputs, filters, `CrmRole`) |
|
|
45
|
-
| `@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
46
|
| `@clivly/core/view-compiler` | `compileEntityView`, `compileEntityViews`, `explainEntitiesConfig`, `DEFAULT_VIEW_PREFIX` |
|
|
47
47
|
| `@clivly/core/mappings-config` | `mappingsToEntitiesConfig`, `EntityMappingRow` — turn `crm_entity_mappings` rows into an entities config |
|
|
48
48
|
| `@clivly/core/mapping-form` | `buildFilterFromDiscriminator`, `buildRelationships`, `relationshipViaSignature` — mapping-UI helpers |
|
|
@@ -64,12 +64,30 @@ import { discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
|
64
64
|
import * as schema from "./db/schema";
|
|
65
65
|
|
|
66
66
|
const tables = discoverFromDrizzle(schema);
|
|
67
|
-
// → [
|
|
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
|
+
// ]
|
|
68
81
|
```
|
|
69
82
|
|
|
70
83
|
Column names are the **real DB names** (`full_name`), not the Drizzle property
|
|
71
84
|
keys (`fullName`).
|
|
72
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
|
+
|
|
73
91
|
### Defining an entity config
|
|
74
92
|
|
|
75
93
|
```ts
|
|
@@ -101,6 +119,73 @@ function useAdapter(crm: CRMAdapter) {
|
|
|
101
119
|
}
|
|
102
120
|
```
|
|
103
121
|
|
|
122
|
+
## Derived objects (projections)
|
|
123
|
+
|
|
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
|
+
|
|
104
189
|
## Advanced: the `$raw` escape hatch
|
|
105
190
|
|
|
106
191
|
Entity `filter`s and relationship `via`s accept a `$raw` form — an opaque SQL
|
package/dist/drizzle.d.cts
CHANGED
package/dist/drizzle.d.mts
CHANGED
|
@@ -494,8 +494,53 @@ function defaultWarn(message) {
|
|
|
494
494
|
* a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
|
|
495
495
|
* needs the discovered schema; see `validateEntitiesConfig`.
|
|
496
496
|
*/
|
|
497
|
+
/**
|
|
498
|
+
* The `fields` entry a projection binding implies, qualified as `ref.column`.
|
|
499
|
+
* Qualified rather than bare: two bindings can pull the same column name from
|
|
500
|
+
* different tables (`cl.full_name` and `adv.full_name`), which a bare column
|
|
501
|
+
* name cannot tell apart.
|
|
502
|
+
*/
|
|
503
|
+
function projectionFields(projection) {
|
|
504
|
+
const fields = {};
|
|
505
|
+
for (const [name, binding] of Object.entries(projection.fieldBindings)) fields[name] = `${binding.from.table}.${binding.from.column}`;
|
|
506
|
+
return fields;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* `fields` duplicates `projection.fieldBindings`, and only the bindings are
|
|
510
|
+
* compiled — so a hand-written `fields` that drifts is both silent and
|
|
511
|
+
* invisible. Derive it when omitted, and reject it when it disagrees.
|
|
512
|
+
*/
|
|
513
|
+
function normalizeProjectionFields(config) {
|
|
514
|
+
const entities = {};
|
|
515
|
+
for (const [key, entity] of Object.entries(config.entities ?? {})) {
|
|
516
|
+
const projection = entity?.projection;
|
|
517
|
+
if (!projection?.fieldBindings) {
|
|
518
|
+
entities[key] = entity;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const derived = projectionFields(projection);
|
|
522
|
+
const declared = entity.fields;
|
|
523
|
+
if (!declared) {
|
|
524
|
+
entities[key] = {
|
|
525
|
+
...entity,
|
|
526
|
+
fields: derived
|
|
527
|
+
};
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const drifted = [.../* @__PURE__ */ new Set([...Object.keys(derived), ...Object.keys(declared)])].filter((name) => declared[name] !== derived[name]).sort();
|
|
531
|
+
if (drifted.length > 0) {
|
|
532
|
+
const detail = drifted.map((name) => `"${name}": fields has ${declared[name] === void 0 ? "no entry" : `"${declared[name]}"`}, bindings imply ${derived[name] === void 0 ? "no entry" : `"${derived[name]}"`}`).join("; ");
|
|
533
|
+
throw new Error(`Clivly config: entity "${key}" has a \`fields\` map that disagrees with \`projection.fieldBindings\` — ${detail}. Only the bindings are compiled, so the \`fields\` copy would be silently ignored. Omit \`fields\` entirely and it will be derived.`);
|
|
534
|
+
}
|
|
535
|
+
entities[key] = entity;
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
...config,
|
|
539
|
+
entities
|
|
540
|
+
};
|
|
541
|
+
}
|
|
497
542
|
function defineClivlyConfig(config, options) {
|
|
498
|
-
const parsed = entitiesConfigSchema.parse(config);
|
|
543
|
+
const parsed = entitiesConfigSchema.parse(normalizeProjectionFields(config));
|
|
499
544
|
const issues = canonicalFieldIssues(parsed);
|
|
500
545
|
if (issues.length > 0) {
|
|
501
546
|
if (options?.strict) throw new ClivlyConfigError(issues);
|
|
@@ -618,4 +663,4 @@ function validateEntitiesConfig(config, schema) {
|
|
|
618
663
|
};
|
|
619
664
|
}
|
|
620
665
|
//#endregion
|
|
621
|
-
export {
|
|
666
|
+
export { coversUniqueKey as _, MATERIALIZING_CONCEPTS as a, filterSchema as c, relationshipsSchema as d, validateEntitiesConfig as f, columnMetaFor as g, joinFansOut as h, ClivlyConfigError as i, projectionFields as l, fanOutJoins as m, CRM_CONCEPTS as n, defineClivlyConfig as o, validateProjection as p, CUSTOM_CONCEPT as r, entitiesConfigSchema as s, CANONICAL_FIELDS as t, relationshipSchema as u, hasRichMeta as v, primaryKeyColumns as y };
|