@clivly/core 0.9.0 → 0.9.2
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 +91 -3
- package/dist/drizzle.d.cts +1 -1
- package/dist/drizzle.d.mts +1 -1
- package/dist/{entity-config-PoOGzvpN.cjs → entity-config-Bfq3OkCU.cjs} +88 -13
- package/dist/{entity-config-Tb5bbKLj.mjs → entity-config-DFJ8L9jm.mjs} +77 -14
- package/dist/entity-config-buOIVtMM.d.cts +580 -0
- package/dist/entity-config-buOIVtMM.d.mts +580 -0
- package/dist/entity-config.cjs +3 -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/integration-state.cjs +98 -26
- package/dist/integration-state.d.cts +94 -7
- package/dist/integration-state.d.mts +94 -7
- package/dist/integration-state.mjs +96 -27
- package/dist/mapping-form.d.cts +1 -1
- package/dist/mapping-form.d.mts +1 -1
- package/dist/mappings-config.cjs +25 -1
- package/dist/mappings-config.d.cts +85 -2
- package/dist/mappings-config.d.mts +85 -2
- package/dist/mappings-config.mjs +25 -2
- 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
|
@@ -14,6 +14,9 @@ the view compiler that turns a mapping into SQL. Its only runtime dependency is
|
|
|
14
14
|
> to import it from. Reach for `@clivly/core` when you are building an adapter
|
|
15
15
|
> or working with the entity config outside an SDK instance.
|
|
16
16
|
|
|
17
|
+
Connection and presence transport live in `@clivly/sdk`, not this package.
|
|
18
|
+
That includes the opt-in WebSocket presence transport and its HTTP fallback.
|
|
19
|
+
|
|
17
20
|
## Prerequisites
|
|
18
21
|
|
|
19
22
|
| Requirement | Version / note |
|
|
@@ -37,12 +40,12 @@ peer), `/entity-heuristics`, `/mapping-score`, `/integration-state` or
|
|
|
37
40
|
|
|
38
41
|
| Entry | Contents |
|
|
39
42
|
| --- | --- |
|
|
40
|
-
| `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, shared types |
|
|
43
|
+
| `@clivly/core` | The re-exported surface: contracts, entity config, mapping form, sync engine, view compiler, projections, shared types |
|
|
41
44
|
| `@clivly/core/adapter` | `CRMAdapter` — the interface every ORM implementation fulfils (org-scoped; contacts, companies, deals, tasks, activities) |
|
|
42
45
|
| `@clivly/core/auth-adapter` | `ClivlyAuthAdapter`, `ClivlyUser`, `authCustom()`, `MembershipResolver`, `CrmMembership` — pluggable host-app identity + the `crm_members` authorization layer |
|
|
43
46
|
| `@clivly/core/entity-config` | `defineClivlyConfig`, `validateEntitiesConfig`, `entitiesConfigSchema`, `CANONICAL_FIELDS`, `CRM_CONCEPTS`, `ClivlyConfigError`, and the config types |
|
|
44
47
|
| `@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 |
|
|
48
|
+
| `@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
49
|
| `@clivly/core/view-compiler` | `compileEntityView`, `compileEntityViews`, `explainEntitiesConfig`, `DEFAULT_VIEW_PREFIX` |
|
|
47
50
|
| `@clivly/core/mappings-config` | `mappingsToEntitiesConfig`, `EntityMappingRow` — turn `crm_entity_mappings` rows into an entities config |
|
|
48
51
|
| `@clivly/core/mapping-form` | `buildFilterFromDiscriminator`, `buildRelationships`, `relationshipViaSignature` — mapping-UI helpers |
|
|
@@ -64,12 +67,30 @@ import { discoverFromDrizzle } from "@clivly/core/drizzle";
|
|
|
64
67
|
import * as schema from "./db/schema";
|
|
65
68
|
|
|
66
69
|
const tables = discoverFromDrizzle(schema);
|
|
67
|
-
// → [
|
|
70
|
+
// → [
|
|
71
|
+
// {
|
|
72
|
+
// name: "participants",
|
|
73
|
+
// columns: ["id", "full_name", "email", …],
|
|
74
|
+
// columnsMeta: [
|
|
75
|
+
// { name: "id", type: "uuid", nullable: false, isPrimaryKey: true, isForeignKey: false },
|
|
76
|
+
// { name: "org_id", type: "uuid", nullable: false, isPrimaryKey: false,
|
|
77
|
+
// isForeignKey: true, references: { table: "organizations", column: "id" } },
|
|
78
|
+
// …
|
|
79
|
+
// ],
|
|
80
|
+
// uniqueConstraints: [["id"], ["email"]],
|
|
81
|
+
// },
|
|
82
|
+
// …
|
|
83
|
+
// ]
|
|
68
84
|
```
|
|
69
85
|
|
|
70
86
|
Column names are the **real DB names** (`full_name`), not the Drizzle property
|
|
71
87
|
keys (`fullName`).
|
|
72
88
|
|
|
89
|
+
`columnsMeta` and `uniqueConstraints` are what [derived
|
|
90
|
+
objects](#derived-objects-projections) validate against — without them a
|
|
91
|
+
projection cannot be authored, so keep this wired up rather than hand-writing
|
|
92
|
+
`{ name, columns }` pairs.
|
|
93
|
+
|
|
73
94
|
### Defining an entity config
|
|
74
95
|
|
|
75
96
|
```ts
|
|
@@ -101,6 +122,73 @@ function useAdapter(crm: CRMAdapter) {
|
|
|
101
122
|
}
|
|
102
123
|
```
|
|
103
124
|
|
|
125
|
+
## Derived objects (projections)
|
|
126
|
+
|
|
127
|
+
An ordinary entity maps one table. A **projection** derives a custom object from
|
|
128
|
+
a base table plus explicit joins, so an object that spans tables — an
|
|
129
|
+
`Enrollment` built from `enrollments`, `students` and `courses` — can be
|
|
130
|
+
expressed at all.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { validateProjection, type ProjectionMapping } from "@clivly/core";
|
|
134
|
+
|
|
135
|
+
const enrollment: ProjectionMapping = {
|
|
136
|
+
version: 1,
|
|
137
|
+
kind: "projection",
|
|
138
|
+
entityKey: "enrollment",
|
|
139
|
+
baseTable: "enrollments",
|
|
140
|
+
joins: [
|
|
141
|
+
{
|
|
142
|
+
key: "stu",
|
|
143
|
+
table: "students",
|
|
144
|
+
type: "left",
|
|
145
|
+
on: [{ left: { table: "stu", column: "id" }, op: "eq",
|
|
146
|
+
right: { table: "base", column: "student_id" } }],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
fieldBindings: {
|
|
150
|
+
student_name: { from: { table: "stu", column: "full_name" } },
|
|
151
|
+
term: { from: { table: "base", column: "term" } },
|
|
152
|
+
},
|
|
153
|
+
identity: { kind: "base_pk" },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const errors = validateProjection(enrollment, tables, "projection");
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Refer to the base table as `base` and each join by its `key`. Validation is
|
|
160
|
+
**fail-closed**: a table whose discovery payload lacks `columnsMeta` /
|
|
161
|
+
`uniqueConstraints` is rejected rather than assumed safe, because identity
|
|
162
|
+
uniqueness cannot be proven without them.
|
|
163
|
+
|
|
164
|
+
### Fan-out
|
|
165
|
+
|
|
166
|
+
A join that does not cover a unique key of the joined table can match many rows
|
|
167
|
+
per base row. That is allowed — but the base table's primary key is then no
|
|
168
|
+
longer unique per output row, so validation enforces: at most one fan-out join,
|
|
169
|
+
no `base_pk` identity on a fanning join, and an identity that includes a unique
|
|
170
|
+
key of the fanning table. `fanOutJoins` reports which joins fan out.
|
|
171
|
+
|
|
172
|
+
Composite identities concatenate their parts with `::`, so every part must be
|
|
173
|
+
`NOT NULL` (`CONCAT_WS` skips nulls) and non-text (a value containing `::` could
|
|
174
|
+
collide).
|
|
175
|
+
|
|
176
|
+
### Building one interactively
|
|
177
|
+
|
|
178
|
+
`ProjectionDraft` and its helpers exist so a builder UI stays presentational —
|
|
179
|
+
every rule is a pure function of draft + schema:
|
|
180
|
+
|
|
181
|
+
| Helper | Purpose |
|
|
182
|
+
| --- | --- |
|
|
183
|
+
| `emptyProjectionDraft(baseTable)` | A blank draft |
|
|
184
|
+
| `suggestJoinConditions(base, joinTable, schema, key)` | Pre-fill a join's `ON` from foreign-key metadata |
|
|
185
|
+
| `draftToProjection(draft)` | `ProjectionMapping`, or `null` while incomplete |
|
|
186
|
+
| `draftErrors(draft, schema)` | Validation errors for the current draft |
|
|
187
|
+
| `describeCardinality(draft, schema)` | Per-join `{ joinKey, fansOut }` for inline hints |
|
|
188
|
+
|
|
189
|
+
Full guide, including how to choose an identity and the current limits:
|
|
190
|
+
[`docs/guides/derived-objects.md`](https://github.com/amani-joseph/clivly.com/blob/master/docs/guides/derived-objects.md).
|
|
191
|
+
|
|
104
192
|
## Advanced: the `$raw` escape hatch
|
|
105
193
|
|
|
106
194
|
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
|
@@ -438,10 +438,23 @@ const entitySchema = zod.z.strictObject({
|
|
|
438
438
|
relationships: zod.z.record(zod.z.string(), relationshipSchema).optional(),
|
|
439
439
|
/**
|
|
440
440
|
* For a "custom" entity, the crm_object_types row this materializes into.
|
|
441
|
-
*
|
|
441
|
+
* Server-generated, so pasting one into config couples the file to a
|
|
442
|
+
* specific workspace. Prefer declaring `objectType` and letting
|
|
443
|
+
* `clivly push-schema` resolve the id by slug.
|
|
442
444
|
*/
|
|
443
445
|
targetObjectTypeId: zod.z.string().min(1).optional(),
|
|
444
446
|
/**
|
|
447
|
+
* Declares the custom object this entity materializes into, so config is
|
|
448
|
+
* self-contained: the entity key is the slug (unique per org and immutable),
|
|
449
|
+
* and `clivly push-schema` creates the object type if the workspace does not
|
|
450
|
+
* have it yet. An alternative to pasting a `targetObjectTypeId`.
|
|
451
|
+
*/
|
|
452
|
+
objectType: zod.z.strictObject({
|
|
453
|
+
name: zod.z.string().min(1),
|
|
454
|
+
pluralName: zod.z.string().min(1).optional(),
|
|
455
|
+
iconKey: zod.z.string().min(1).optional()
|
|
456
|
+
}).optional(),
|
|
457
|
+
/**
|
|
445
458
|
* Advanced mapping: derive this entity from a base table plus explicit joins
|
|
446
459
|
* instead of a single source table. Valid only for `concept: "custom"`.
|
|
447
460
|
*
|
|
@@ -483,19 +496,69 @@ function defaultWarn(message) {
|
|
|
483
496
|
console.warn(message);
|
|
484
497
|
}
|
|
485
498
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
499
|
+
* The custom objects a config declares, keyed by entity slug. Entities that
|
|
500
|
+
* already carry an explicit `targetObjectTypeId` are omitted: their object type
|
|
501
|
+
* exists server-side and re-declaring it would be a no-op at best.
|
|
502
|
+
*/
|
|
503
|
+
function customObjectDeclarations(config) {
|
|
504
|
+
const declarations = [];
|
|
505
|
+
for (const [slug, entity] of Object.entries(config.entities)) {
|
|
506
|
+
if (entity.concept !== "custom" || entity.targetObjectTypeId) continue;
|
|
507
|
+
if (!entity.objectType) continue;
|
|
508
|
+
declarations.push({
|
|
509
|
+
slug,
|
|
510
|
+
...entity.objectType
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return declarations;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The `fields` entry a projection binding implies, qualified as `ref.column`.
|
|
517
|
+
* Qualified rather than bare: two bindings can pull the same column name from
|
|
518
|
+
* different tables (`cl.full_name` and `adv.full_name`), which a bare column
|
|
519
|
+
* name cannot tell apart.
|
|
520
|
+
*/
|
|
521
|
+
function projectionFields(projection) {
|
|
522
|
+
const fields = {};
|
|
523
|
+
for (const [name, binding] of Object.entries(projection.fieldBindings)) fields[name] = `${binding.from.table}.${binding.from.column}`;
|
|
524
|
+
return fields;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* `fields` duplicates `projection.fieldBindings`, and only the bindings are
|
|
528
|
+
* compiled — so a hand-written `fields` that drifts is both silent and
|
|
529
|
+
* invisible. Derive it when omitted, and reject it when it disagrees.
|
|
496
530
|
*/
|
|
531
|
+
function normalizeProjectionFields(config) {
|
|
532
|
+
const entities = {};
|
|
533
|
+
for (const [key, entity] of Object.entries(config.entities ?? {})) {
|
|
534
|
+
const projection = entity?.projection;
|
|
535
|
+
if (!projection?.fieldBindings) {
|
|
536
|
+
entities[key] = entity;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const derived = projectionFields(projection);
|
|
540
|
+
const declared = entity.fields;
|
|
541
|
+
if (!declared) {
|
|
542
|
+
entities[key] = {
|
|
543
|
+
...entity,
|
|
544
|
+
fields: derived
|
|
545
|
+
};
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const drifted = [.../* @__PURE__ */ new Set([...Object.keys(derived), ...Object.keys(declared)])].filter((name) => declared[name] !== derived[name]).sort();
|
|
549
|
+
if (drifted.length > 0) {
|
|
550
|
+
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("; ");
|
|
551
|
+
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.`);
|
|
552
|
+
}
|
|
553
|
+
entities[key] = entity;
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
...config,
|
|
557
|
+
entities
|
|
558
|
+
};
|
|
559
|
+
}
|
|
497
560
|
function defineClivlyConfig(config, options) {
|
|
498
|
-
const parsed = entitiesConfigSchema.parse(config);
|
|
561
|
+
const parsed = entitiesConfigSchema.parse(normalizeProjectionFields(config));
|
|
499
562
|
const issues = canonicalFieldIssues(parsed);
|
|
500
563
|
if (issues.length > 0) {
|
|
501
564
|
if (options?.strict) throw new ClivlyConfigError(issues);
|
|
@@ -588,7 +651,7 @@ function validateEntity(v, config, key, entity, schema) {
|
|
|
588
651
|
}
|
|
589
652
|
if (entity.projection) {
|
|
590
653
|
if (entity.concept !== "custom") v.report(`${base}.projection`, `projection mappings are only supported for concept "${CUSTOM_CONCEPT}"`);
|
|
591
|
-
if (!entity.targetObjectTypeId) v.report(`${base}.targetObjectTypeId`, "a projection mapping must target a custom object type");
|
|
654
|
+
if (!(entity.targetObjectTypeId || entity.objectType)) v.report(`${base}.targetObjectTypeId`, "a projection mapping must target a custom object type — declare `objectType: { name: \"…\" }` (recommended: the entity key becomes its slug) or set an explicit `targetObjectTypeId`");
|
|
592
655
|
if (entity.source !== entity.projection.baseTable) v.report(`${base}.source`, `"source" ("${entity.source}") must match "projection.baseTable" ("${entity.projection.baseTable}") — the view compiler reads baseTable and the sync engine reads source, so a mismatch makes them disagree about which table this entity is`);
|
|
593
656
|
for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
|
|
594
657
|
validateReadOnly(v, base, entity);
|
|
@@ -660,6 +723,12 @@ Object.defineProperty(exports, "coversUniqueKey", {
|
|
|
660
723
|
return coversUniqueKey;
|
|
661
724
|
}
|
|
662
725
|
});
|
|
726
|
+
Object.defineProperty(exports, "customObjectDeclarations", {
|
|
727
|
+
enumerable: true,
|
|
728
|
+
get: function() {
|
|
729
|
+
return customObjectDeclarations;
|
|
730
|
+
}
|
|
731
|
+
});
|
|
663
732
|
Object.defineProperty(exports, "defineClivlyConfig", {
|
|
664
733
|
enumerable: true,
|
|
665
734
|
get: function() {
|
|
@@ -702,6 +771,12 @@ Object.defineProperty(exports, "primaryKeyColumns", {
|
|
|
702
771
|
return primaryKeyColumns;
|
|
703
772
|
}
|
|
704
773
|
});
|
|
774
|
+
Object.defineProperty(exports, "projectionFields", {
|
|
775
|
+
enumerable: true,
|
|
776
|
+
get: function() {
|
|
777
|
+
return projectionFields;
|
|
778
|
+
}
|
|
779
|
+
});
|
|
705
780
|
Object.defineProperty(exports, "relationshipSchema", {
|
|
706
781
|
enumerable: true,
|
|
707
782
|
get: function() {
|
|
@@ -438,10 +438,23 @@ const entitySchema = z.strictObject({
|
|
|
438
438
|
relationships: z.record(z.string(), relationshipSchema).optional(),
|
|
439
439
|
/**
|
|
440
440
|
* For a "custom" entity, the crm_object_types row this materializes into.
|
|
441
|
-
*
|
|
441
|
+
* Server-generated, so pasting one into config couples the file to a
|
|
442
|
+
* specific workspace. Prefer declaring `objectType` and letting
|
|
443
|
+
* `clivly push-schema` resolve the id by slug.
|
|
442
444
|
*/
|
|
443
445
|
targetObjectTypeId: z.string().min(1).optional(),
|
|
444
446
|
/**
|
|
447
|
+
* Declares the custom object this entity materializes into, so config is
|
|
448
|
+
* self-contained: the entity key is the slug (unique per org and immutable),
|
|
449
|
+
* and `clivly push-schema` creates the object type if the workspace does not
|
|
450
|
+
* have it yet. An alternative to pasting a `targetObjectTypeId`.
|
|
451
|
+
*/
|
|
452
|
+
objectType: z.strictObject({
|
|
453
|
+
name: z.string().min(1),
|
|
454
|
+
pluralName: z.string().min(1).optional(),
|
|
455
|
+
iconKey: z.string().min(1).optional()
|
|
456
|
+
}).optional(),
|
|
457
|
+
/**
|
|
445
458
|
* Advanced mapping: derive this entity from a base table plus explicit joins
|
|
446
459
|
* instead of a single source table. Valid only for `concept: "custom"`.
|
|
447
460
|
*
|
|
@@ -483,19 +496,69 @@ function defaultWarn(message) {
|
|
|
483
496
|
console.warn(message);
|
|
484
497
|
}
|
|
485
498
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
499
|
+
* The custom objects a config declares, keyed by entity slug. Entities that
|
|
500
|
+
* already carry an explicit `targetObjectTypeId` are omitted: their object type
|
|
501
|
+
* exists server-side and re-declaring it would be a no-op at best.
|
|
502
|
+
*/
|
|
503
|
+
function customObjectDeclarations(config) {
|
|
504
|
+
const declarations = [];
|
|
505
|
+
for (const [slug, entity] of Object.entries(config.entities)) {
|
|
506
|
+
if (entity.concept !== "custom" || entity.targetObjectTypeId) continue;
|
|
507
|
+
if (!entity.objectType) continue;
|
|
508
|
+
declarations.push({
|
|
509
|
+
slug,
|
|
510
|
+
...entity.objectType
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return declarations;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The `fields` entry a projection binding implies, qualified as `ref.column`.
|
|
517
|
+
* Qualified rather than bare: two bindings can pull the same column name from
|
|
518
|
+
* different tables (`cl.full_name` and `adv.full_name`), which a bare column
|
|
519
|
+
* name cannot tell apart.
|
|
520
|
+
*/
|
|
521
|
+
function projectionFields(projection) {
|
|
522
|
+
const fields = {};
|
|
523
|
+
for (const [name, binding] of Object.entries(projection.fieldBindings)) fields[name] = `${binding.from.table}.${binding.from.column}`;
|
|
524
|
+
return fields;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* `fields` duplicates `projection.fieldBindings`, and only the bindings are
|
|
528
|
+
* compiled — so a hand-written `fields` that drifts is both silent and
|
|
529
|
+
* invisible. Derive it when omitted, and reject it when it disagrees.
|
|
496
530
|
*/
|
|
531
|
+
function normalizeProjectionFields(config) {
|
|
532
|
+
const entities = {};
|
|
533
|
+
for (const [key, entity] of Object.entries(config.entities ?? {})) {
|
|
534
|
+
const projection = entity?.projection;
|
|
535
|
+
if (!projection?.fieldBindings) {
|
|
536
|
+
entities[key] = entity;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const derived = projectionFields(projection);
|
|
540
|
+
const declared = entity.fields;
|
|
541
|
+
if (!declared) {
|
|
542
|
+
entities[key] = {
|
|
543
|
+
...entity,
|
|
544
|
+
fields: derived
|
|
545
|
+
};
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const drifted = [.../* @__PURE__ */ new Set([...Object.keys(derived), ...Object.keys(declared)])].filter((name) => declared[name] !== derived[name]).sort();
|
|
549
|
+
if (drifted.length > 0) {
|
|
550
|
+
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("; ");
|
|
551
|
+
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.`);
|
|
552
|
+
}
|
|
553
|
+
entities[key] = entity;
|
|
554
|
+
}
|
|
555
|
+
return {
|
|
556
|
+
...config,
|
|
557
|
+
entities
|
|
558
|
+
};
|
|
559
|
+
}
|
|
497
560
|
function defineClivlyConfig(config, options) {
|
|
498
|
-
const parsed = entitiesConfigSchema.parse(config);
|
|
561
|
+
const parsed = entitiesConfigSchema.parse(normalizeProjectionFields(config));
|
|
499
562
|
const issues = canonicalFieldIssues(parsed);
|
|
500
563
|
if (issues.length > 0) {
|
|
501
564
|
if (options?.strict) throw new ClivlyConfigError(issues);
|
|
@@ -588,7 +651,7 @@ function validateEntity(v, config, key, entity, schema) {
|
|
|
588
651
|
}
|
|
589
652
|
if (entity.projection) {
|
|
590
653
|
if (entity.concept !== "custom") v.report(`${base}.projection`, `projection mappings are only supported for concept "${CUSTOM_CONCEPT}"`);
|
|
591
|
-
if (!entity.targetObjectTypeId) v.report(`${base}.targetObjectTypeId`, "a projection mapping must target a custom object type");
|
|
654
|
+
if (!(entity.targetObjectTypeId || entity.objectType)) v.report(`${base}.targetObjectTypeId`, "a projection mapping must target a custom object type — declare `objectType: { name: \"…\" }` (recommended: the entity key becomes its slug) or set an explicit `targetObjectTypeId`");
|
|
592
655
|
if (entity.source !== entity.projection.baseTable) v.report(`${base}.source`, `"source" ("${entity.source}") must match "projection.baseTable" ("${entity.projection.baseTable}") — the view compiler reads baseTable and the sync engine reads source, so a mismatch makes them disagree about which table this entity is`);
|
|
593
656
|
for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
|
|
594
657
|
validateReadOnly(v, base, entity);
|
|
@@ -618,4 +681,4 @@ function validateEntitiesConfig(config, schema) {
|
|
|
618
681
|
};
|
|
619
682
|
}
|
|
620
683
|
//#endregion
|
|
621
|
-
export {
|
|
684
|
+
export { columnMetaFor as _, MATERIALIZING_CONCEPTS as a, primaryKeyColumns as b, entitiesConfigSchema as c, relationshipSchema as d, relationshipsSchema as f, joinFansOut as g, fanOutJoins as h, ClivlyConfigError as i, filterSchema as l, validateProjection as m, CRM_CONCEPTS as n, customObjectDeclarations as o, validateEntitiesConfig as p, CUSTOM_CONCEPT as r, defineClivlyConfig as s, CANONICAL_FIELDS as t, projectionFields as u, coversUniqueKey as v, hasRichMeta as y };
|