@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.
Files changed (42) hide show
  1. package/README.md +91 -3
  2. package/dist/drizzle.d.cts +1 -1
  3. package/dist/drizzle.d.mts +1 -1
  4. package/dist/{entity-config-PoOGzvpN.cjs → entity-config-Bfq3OkCU.cjs} +88 -13
  5. package/dist/{entity-config-Tb5bbKLj.mjs → entity-config-DFJ8L9jm.mjs} +77 -14
  6. package/dist/entity-config-buOIVtMM.d.cts +580 -0
  7. package/dist/entity-config-buOIVtMM.d.mts +580 -0
  8. package/dist/entity-config.cjs +3 -1
  9. package/dist/entity-config.d.cts +2 -414
  10. package/dist/entity-config.d.mts +2 -414
  11. package/dist/entity-config.mjs +2 -2
  12. package/dist/entity-heuristics.cjs +1 -1
  13. package/dist/entity-heuristics.d.cts +1 -1
  14. package/dist/entity-heuristics.d.mts +1 -1
  15. package/dist/entity-heuristics.mjs +1 -1
  16. package/dist/index.cjs +2 -2
  17. package/dist/index.d.cts +2 -2
  18. package/dist/index.d.mts +2 -2
  19. package/dist/index.mjs +2 -2
  20. package/dist/integration-state.cjs +98 -26
  21. package/dist/integration-state.d.cts +94 -7
  22. package/dist/integration-state.d.mts +94 -7
  23. package/dist/integration-state.mjs +96 -27
  24. package/dist/mapping-form.d.cts +1 -1
  25. package/dist/mapping-form.d.mts +1 -1
  26. package/dist/mappings-config.cjs +25 -1
  27. package/dist/mappings-config.d.cts +85 -2
  28. package/dist/mappings-config.d.mts +85 -2
  29. package/dist/mappings-config.mjs +25 -2
  30. package/dist/sync-engine.cjs +1 -1
  31. package/dist/sync-engine.d.cts +1 -1
  32. package/dist/sync-engine.d.mts +1 -1
  33. package/dist/sync-engine.mjs +1 -1
  34. package/dist/{view-compiler-BwZ9UgLp.cjs → view-compiler-BpNI8Ttw.cjs} +22 -0
  35. package/dist/{view-compiler-BKrBA_II.mjs → view-compiler-CcqsGxv4.mjs} +17 -1
  36. package/dist/view-compiler.cjs +2 -1
  37. package/dist/view-compiler.d.cts +12 -2
  38. package/dist/view-compiler.d.mts +12 -2
  39. package/dist/view-compiler.mjs +2 -2
  40. package/package.json +1 -1
  41. package/dist/mappings-config-B0W5bVZj.d.mts +0 -168
  42. 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
- // → [{ name: "participants", columns: ["id", "full_name", "email", …] }, …]
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
@@ -1,4 +1,4 @@
1
- import { DiscoveredColumnMeta, DiscoveredSchemaTable } from "./entity-config.cjs";
1
+ import { m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-buOIVtMM.cjs";
2
2
  import { Table } from "drizzle-orm";
3
3
  //#region src/drizzle.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { DiscoveredColumnMeta, DiscoveredSchemaTable } from "./entity-config.mjs";
1
+ import { m as DiscoveredSchemaTable, p as DiscoveredColumnMeta } from "./entity-config-buOIVtMM.mjs";
2
2
  import { Table } from "drizzle-orm";
3
3
  //#region src/drizzle.d.ts
4
4
  /**
@@ -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
- * Required for custom (enforced at the API layer), unused for standard concepts.
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
- * Structural helper for `clivly.config.ts`. Gives full type inference on the
487
- * literal you pass and eagerly parses it, so shape errors surface at config
488
- * load rather than at sync time.
489
- *
490
- * It also catches the silent-data-loss footgun: a non-canonical field key for a
491
- * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
492
- * destination column, so the store would write an empty value. By default these
493
- * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
494
- * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
495
- * needs the discovered schema; see `validateEntitiesConfig`.
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
- * Required for custom (enforced at the API layer), unused for standard concepts.
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
- * Structural helper for `clivly.config.ts`. Gives full type inference on the
487
- * literal you pass and eagerly parses it, so shape errors surface at config
488
- * load rather than at sync time.
489
- *
490
- * It also catches the silent-data-loss footgun: a non-canonical field key for a
491
- * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
492
- * destination column, so the store would write an empty value. By default these
493
- * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
494
- * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
495
- * needs the discovered schema; see `validateEntitiesConfig`.
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 { hasRichMeta as _, MATERIALIZING_CONCEPTS as a, filterSchema as c, validateEntitiesConfig as d, validateProjection as f, coversUniqueKey as g, columnMetaFor as h, ClivlyConfigError as i, relationshipSchema as l, joinFansOut as m, CRM_CONCEPTS as n, defineClivlyConfig as o, fanOutJoins as p, CUSTOM_CONCEPT as r, entitiesConfigSchema as s, CANONICAL_FIELDS as t, relationshipsSchema as u, primaryKeyColumns as v };
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 };