clivly 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 (98) hide show
  1. package/README.md +19 -7
  2. package/core-drizzle.d.cts +1 -1
  3. package/core-drizzle.d.mts +1 -1
  4. package/core-entity-config.cjs +3 -1
  5. package/core-entity-config.d.cts +2 -2
  6. package/core-entity-config.d.mts +2 -2
  7. package/core-entity-config.mjs +2 -2
  8. package/core-entity-heuristics.d.cts +1 -1
  9. package/core-entity-heuristics.d.mts +1 -1
  10. package/core-mapping-form.d.cts +1 -1
  11. package/core-mapping-form.d.mts +1 -1
  12. package/core-mappings-config.cjs +2 -1
  13. package/core-mappings-config.d.cts +2 -2
  14. package/core-mappings-config.d.mts +2 -2
  15. package/core-mappings-config.mjs +2 -2
  16. package/core-sync-engine.cjs +1 -1
  17. package/core-sync-engine.d.cts +1 -1
  18. package/core-sync-engine.d.mts +1 -1
  19. package/core-sync-engine.mjs +1 -1
  20. package/core-view-compiler.cjs +2 -1
  21. package/core-view-compiler.d.cts +2 -2
  22. package/core-view-compiler.d.mts +2 -2
  23. package/core-view-compiler.mjs +2 -2
  24. package/core.cjs +5 -5
  25. package/core.d.cts +6 -6
  26. package/core.d.mts +6 -6
  27. package/core.mjs +5 -5
  28. package/{drizzle-BhN5M9zt.d.cts → drizzle-CiK8MMEZ.d.mts} +1 -1
  29. package/{drizzle-kBULBAOa.d.mts → drizzle-DEtRrqtc.d.cts} +1 -1
  30. package/drizzle.d.cts +2 -2
  31. package/drizzle.d.mts +2 -2
  32. package/{entity-config-C4Yf_fPj.d.cts → entity-config-B2RA53nw.d.cts} +168 -2
  33. package/{entity-config-C4Yf_fPj.d.mts → entity-config-B2RA53nw.d.mts} +168 -2
  34. package/{entity-config-CsLl-ftC.cjs → entity-config-DtBsl2MQ.cjs} +88 -13
  35. package/{entity-config-D3LmzgLb.mjs → entity-config-kUHri9yA.mjs} +77 -14
  36. package/index.cjs +375 -29
  37. package/index.d.cts +1 -0
  38. package/index.d.mts +1 -0
  39. package/index.mjs +375 -29
  40. package/{internal-MBkNo3bw.d.mts → internal-CDY8g9Ic.d.mts} +1 -2
  41. package/{internal-BARjyN4n.cjs → internal-CwnEG6Wl.cjs} +1 -1
  42. package/{internal-B8ydhsKd.d.cts → internal-Da_dlcQ_.d.cts} +1 -2
  43. package/{internal-DMySVbM0.mjs → internal-Df-PtOH_.mjs} +1 -1
  44. package/internal-drizzle.d.cts +1 -1
  45. package/internal-drizzle.d.mts +1 -1
  46. package/internal-entity-config.cjs +3 -1
  47. package/internal-entity-config.d.cts +2 -2
  48. package/internal-entity-config.d.mts +2 -2
  49. package/internal-entity-config.mjs +2 -2
  50. package/internal-entity-heuristics.cjs +1 -1
  51. package/internal-entity-heuristics.d.cts +1 -1
  52. package/internal-entity-heuristics.d.mts +1 -1
  53. package/internal-entity-heuristics.mjs +1 -1
  54. package/internal-mapping-form.d.cts +1 -1
  55. package/internal-mapping-form.d.mts +1 -1
  56. package/internal-mappings-config.cjs +2 -1
  57. package/internal-mappings-config.d.cts +2 -2
  58. package/internal-mappings-config.d.mts +2 -2
  59. package/internal-mappings-config.mjs +2 -2
  60. package/internal-sync-engine.cjs +1 -1
  61. package/internal-sync-engine.d.cts +1 -1
  62. package/internal-sync-engine.d.mts +1 -1
  63. package/internal-sync-engine.mjs +1 -1
  64. package/internal-view-compiler.cjs +2 -1
  65. package/internal-view-compiler.d.cts +2 -2
  66. package/internal-view-compiler.d.mts +2 -2
  67. package/internal-view-compiler.mjs +2 -2
  68. package/internal.cjs +5 -5
  69. package/internal.d.cts +6 -6
  70. package/internal.d.mts +6 -6
  71. package/internal.mjs +5 -5
  72. package/{mapping-form-43bwOTat.d.cts → mapping-form-DH77BXLv.d.mts} +1 -1
  73. package/{mapping-form-DUvWUYCo.d.mts → mapping-form-J0aU6O7l.d.cts} +1 -1
  74. package/{mappings-config-BSSwdHiq.cjs → mappings-config-B0RqFLxu.cjs} +30 -1
  75. package/{mappings-config-bJQT_gSa.mjs → mappings-config-B1b1HVBg.mjs} +25 -2
  76. package/mappings-config-D9dsonoi.d.mts +85 -0
  77. package/mappings-config-RQLGx8ub.d.cts +85 -0
  78. package/{namespace-probe-BSH_p9TK.d.cts → namespace-probe-C6aHjedO.d.cts} +65 -3
  79. package/{namespace-probe-Dsp5uNkS.d.mts → namespace-probe-Dj-0Q3Hb.d.mts} +65 -3
  80. package/package.json +1 -1
  81. package/{sdk-Bj-ZDf4u.cjs → sdk-D4Wp_uxb.cjs} +754 -44
  82. package/{sdk-BvRD2jAp.mjs → sdk-DXV8M_Io.mjs} +754 -44
  83. package/sdk.cjs +4 -2
  84. package/sdk.d.cts +3 -3
  85. package/sdk.d.mts +3 -3
  86. package/sdk.mjs +3 -3
  87. package/{sync-engine-GNqRB8eS.d.cts → sync-engine-BG2O1an2.d.cts} +2 -2
  88. package/{sync-engine-ZsusW-q_.d.mts → sync-engine-DgQbrJON.d.mts} +2 -2
  89. package/{sync-engine-Bg6-SznL.mjs → sync-engine-SuxSzbZ4.mjs} +1 -1
  90. package/{sync-engine-DEJvKw6E.cjs → sync-engine-abE94g6X.cjs} +1 -1
  91. package/{view-compiler-DplczSjF.cjs → view-compiler-Blu4OXJ7.cjs} +22 -0
  92. package/{view-compiler-Cin0neDO.d.mts → view-compiler-Bo7pqXW3.d.mts} +12 -2
  93. package/{view-compiler-DlTpOfZi.mjs → view-compiler-CQx8TRrW.mjs} +17 -1
  94. package/{view-compiler-Ll9cbIJz.d.cts → view-compiler-CmEdAMwj.d.cts} +12 -2
  95. package/vite.d.cts +1 -1
  96. package/vite.d.mts +1 -1
  97. package/mappings-config-BpcR8bBo.d.mts +0 -167
  98. package/mappings-config-xsLMn3I-.d.cts +0 -167
@@ -2120,6 +2120,126 @@ interface ZodReadonly<T extends SomeType = $ZodType> extends _ZodType<$ZodReadon
2120
2120
  }
2121
2121
  declare const ZodReadonly: $constructor<ZodReadonly>;
2122
2122
  //#endregion
2123
+ //#region ../core/src/projection.d.ts
2124
+ /**
2125
+ * Projection-backed custom mappings — one custom object derived from a base
2126
+ * table plus explicit joins.
2127
+ *
2128
+ * A projection is deliberately narrow: equality joins only, direct column
2129
+ * bindings only, no expressions and no aggregation. Everything a user can
2130
+ * express here compiles to a single read-only view whose `id` column is
2131
+ * provably unique per row, which is what lets the sync engine consume it
2132
+ * unchanged.
2133
+ *
2134
+ * Refs address tables symbolically: `"base"` for the base table, or a join's
2135
+ * `key`. Raw table names are never accepted in a ref, so a projection can
2136
+ * never reach a table it did not declare.
2137
+ */
2138
+ /** The reserved ref name for the projection's base table. */
2139
+ declare const BASE_REF = "base";
2140
+ declare const projectionRefSchema: ZodObject<{
2141
+ table: ZodString;
2142
+ column: ZodString;
2143
+ }, $strict>;
2144
+ type ProjectionRef = output<typeof projectionRefSchema>;
2145
+ declare const projectionJoinSchema: ZodObject<{
2146
+ key: ZodString;
2147
+ table: ZodString;
2148
+ type: ZodEnum<{
2149
+ inner: "inner";
2150
+ left: "left";
2151
+ }>;
2152
+ on: ZodArray<ZodObject<{
2153
+ left: ZodObject<{
2154
+ table: ZodString;
2155
+ column: ZodString;
2156
+ }, $strict>;
2157
+ op: ZodLiteral<"eq">;
2158
+ right: ZodObject<{
2159
+ table: ZodString;
2160
+ column: ZodString;
2161
+ }, $strict>;
2162
+ }, $strict>>;
2163
+ }, $strict>;
2164
+ type ProjectionJoin = output<typeof projectionJoinSchema>;
2165
+ declare const projectionFieldBindingSchema: ZodObject<{
2166
+ from: ZodObject<{
2167
+ table: ZodString;
2168
+ column: ZodString;
2169
+ }, $strict>;
2170
+ }, $strict>;
2171
+ type ProjectionFieldBinding = output<typeof projectionFieldBindingSchema>;
2172
+ declare const projectionIdentitySchema: ZodUnion<readonly [ZodObject<{
2173
+ kind: ZodLiteral<"base_pk">;
2174
+ column: ZodOptional<ZodString>;
2175
+ }, $strict>, ZodObject<{
2176
+ kind: ZodLiteral<"column">;
2177
+ from: ZodObject<{
2178
+ table: ZodString;
2179
+ column: ZodString;
2180
+ }, $strict>;
2181
+ }, $strict>, ZodObject<{
2182
+ kind: ZodLiteral<"composite">;
2183
+ parts: ZodArray<ZodObject<{
2184
+ table: ZodString;
2185
+ column: ZodString;
2186
+ }, $strict>>;
2187
+ }, $strict>]>;
2188
+ type ProjectionIdentity = output<typeof projectionIdentitySchema>;
2189
+ declare const projectionSchema: ZodObject<{
2190
+ version: ZodLiteral<1>;
2191
+ kind: ZodLiteral<"projection">;
2192
+ entityKey: ZodString;
2193
+ baseTable: ZodString;
2194
+ joins: ZodArray<ZodObject<{
2195
+ key: ZodString;
2196
+ table: ZodString;
2197
+ type: ZodEnum<{
2198
+ inner: "inner";
2199
+ left: "left";
2200
+ }>;
2201
+ on: ZodArray<ZodObject<{
2202
+ left: ZodObject<{
2203
+ table: ZodString;
2204
+ column: ZodString;
2205
+ }, $strict>;
2206
+ op: ZodLiteral<"eq">;
2207
+ right: ZodObject<{
2208
+ table: ZodString;
2209
+ column: ZodString;
2210
+ }, $strict>;
2211
+ }, $strict>>;
2212
+ }, $strict>>;
2213
+ fieldBindings: ZodRecord<ZodString, ZodObject<{
2214
+ from: ZodObject<{
2215
+ table: ZodString;
2216
+ column: ZodString;
2217
+ }, $strict>;
2218
+ }, $strict>>;
2219
+ identity: ZodUnion<readonly [ZodObject<{
2220
+ kind: ZodLiteral<"base_pk">;
2221
+ column: ZodOptional<ZodString>;
2222
+ }, $strict>, ZodObject<{
2223
+ kind: ZodLiteral<"column">;
2224
+ from: ZodObject<{
2225
+ table: ZodString;
2226
+ column: ZodString;
2227
+ }, $strict>;
2228
+ }, $strict>, ZodObject<{
2229
+ kind: ZodLiteral<"composite">;
2230
+ parts: ZodArray<ZodObject<{
2231
+ table: ZodString;
2232
+ column: ZodString;
2233
+ }, $strict>>;
2234
+ }, $strict>]>;
2235
+ }, $strict>;
2236
+ type ProjectionMapping = output<typeof projectionSchema>;
2237
+ /**
2238
+ * Every column ref an identity depends on. `basePkColumn` is the base table's
2239
+ * discovered PK, used only when a `base_pk` identity omits an explicit column.
2240
+ */
2241
+ declare function identityRefs(identity: ProjectionIdentity, basePkColumn: string): ProjectionRef[];
2242
+ //#endregion
2123
2243
  //#region ../core/src/entity-config.d.ts
2124
2244
  /**
2125
2245
  * Entity Config v2 — the declarative contract that turns Clivly's "schema-aware"
@@ -2312,6 +2432,11 @@ declare const entitySchema: ZodObject<{
2312
2432
  }, $strict>]>;
2313
2433
  }, $strict>>>;
2314
2434
  targetObjectTypeId: ZodOptional<ZodString>;
2435
+ objectType: ZodOptional<ZodObject<{
2436
+ name: ZodString;
2437
+ pluralName: ZodOptional<ZodString>;
2438
+ iconKey: ZodOptional<ZodString>;
2439
+ }, $strict>>;
2315
2440
  projection: ZodOptional<ZodObject<{
2316
2441
  version: ZodLiteral<1>;
2317
2442
  kind: ZodLiteral<"projection">;
@@ -2407,6 +2532,11 @@ declare const entitiesConfigSchema: ZodObject<{
2407
2532
  }, $strict>]>;
2408
2533
  }, $strict>>>;
2409
2534
  targetObjectTypeId: ZodOptional<ZodString>;
2535
+ objectType: ZodOptional<ZodObject<{
2536
+ name: ZodString;
2537
+ pluralName: ZodOptional<ZodString>;
2538
+ iconKey: ZodOptional<ZodString>;
2539
+ }, $strict>>;
2410
2540
  projection: ZodOptional<ZodObject<{
2411
2541
  version: ZodLiteral<1>;
2412
2542
  kind: ZodLiteral<"projection">;
@@ -2457,6 +2587,21 @@ declare const entitiesConfigSchema: ZodObject<{
2457
2587
  }, $strict>>;
2458
2588
  }, $strict>;
2459
2589
  type ClivlyEntitiesConfig = output<typeof entitiesConfigSchema>;
2590
+ /**
2591
+ * What an author may hand to `defineClivlyConfig`. Identical to
2592
+ * `ClivlyEntityConfig` except that a projection entity may omit `fields` —
2593
+ * it is derived from `projection.fieldBindings`. Without a projection `fields`
2594
+ * stays required, so the looser input never weakens the ordinary case.
2595
+ */
2596
+ type ClivlyEntityConfigInput = (ClivlyEntityConfig & {
2597
+ projection?: undefined;
2598
+ }) | (Omit<ClivlyEntityConfig, "fields"> & {
2599
+ fields?: Record<string, string>;
2600
+ projection: ProjectionMapping;
2601
+ });
2602
+ type ClivlyEntitiesConfigInput = Omit<ClivlyEntitiesConfig, "entities"> & {
2603
+ entities: Record<string, ClivlyEntityConfigInput>;
2604
+ };
2460
2605
  /** Thrown by `defineClivlyConfig({ strict: true })` on non-canonical field keys. */
2461
2606
  declare class ClivlyConfigError extends Error {
2462
2607
  readonly issues: ConfigValidationError[];
@@ -2474,7 +2619,28 @@ declare class ClivlyConfigError extends Error {
2474
2619
  * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
2475
2620
  * needs the discovered schema; see `validateEntitiesConfig`.
2476
2621
  */
2477
- declare function defineClivlyConfig(config: ClivlyEntitiesConfig, options?: {
2622
+ /** A custom object the host declares in config, for `push-schema` to ensure. */
2623
+ interface CustomObjectDeclaration {
2624
+ iconKey?: string;
2625
+ name: string;
2626
+ pluralName?: string;
2627
+ /** Unique per org and immutable — the entity key. */
2628
+ slug: string;
2629
+ }
2630
+ /**
2631
+ * The custom objects a config declares, keyed by entity slug. Entities that
2632
+ * already carry an explicit `targetObjectTypeId` are omitted: their object type
2633
+ * exists server-side and re-declaring it would be a no-op at best.
2634
+ */
2635
+ declare function customObjectDeclarations(config: ClivlyEntitiesConfig): CustomObjectDeclaration[];
2636
+ /**
2637
+ * The `fields` entry a projection binding implies, qualified as `ref.column`.
2638
+ * Qualified rather than bare: two bindings can pull the same column name from
2639
+ * different tables (`cl.full_name` and `adv.full_name`), which a bare column
2640
+ * name cannot tell apart.
2641
+ */
2642
+ declare function projectionFields(projection: ProjectionMapping): Record<string, string>;
2643
+ declare function defineClivlyConfig(config: ClivlyEntitiesConfigInput, options?: {
2478
2644
  strict?: boolean;
2479
2645
  onWarn?: (message: string) => void;
2480
2646
  }): ClivlyEntitiesConfig;
@@ -2532,4 +2698,4 @@ interface ConfigValidationResult {
2532
2698
  */
2533
2699
  declare function validateEntitiesConfig(config: ClivlyEntitiesConfig, schema: DiscoveredSchemaTable[]): ConfigValidationResult;
2534
2700
  //#endregion
2535
- export { ZodRecord as A, relationshipsSchema as C, ZodLiteral as D, ZodEnum as E, ZodUnion as M, output as N, ZodObject as O, $strict as P, relationshipSchema as S, ZodArray as T, RelationshipVia as _, ClivlyEntitiesConfig as a, entitiesConfigSchema as b, ConfigValidationResult as c, DiscoveredSchemaTable as d, FilterExpr as f, RelationshipSpec as g, MaterializingConcept as h, ClivlyConfigError as i, ZodString as j, ZodOptional as k, CrmConcept as l, MATERIALIZING_CONCEPTS as m, CRM_CONCEPTS as n, ClivlyEntityConfig as o, FilterValue as p, CUSTOM_CONCEPT as r, ConfigValidationError as s, CANONICAL_FIELDS as t, DiscoveredColumnMeta as u, RelationshipsMap as v, validateEntitiesConfig as w, filterSchema as x, defineClivlyConfig as y };
2701
+ export { BASE_REF as A, defineClivlyConfig as C, relationshipSchema as D, projectionFields as E, ProjectionRef as F, identityRefs as I, projectionSchema as L, ProjectionIdentity as M, ProjectionJoin as N, relationshipsSchema as O, ProjectionMapping as P, customObjectDeclarations as S, filterSchema as T, MATERIALIZING_CONCEPTS as _, ClivlyEntitiesConfig as a, RelationshipVia as b, ClivlyEntityConfigInput as c, CrmConcept as d, CustomObjectDeclaration as f, FilterValue as g, FilterExpr as h, ClivlyConfigError as i, ProjectionFieldBinding as j, validateEntitiesConfig as k, ConfigValidationError as l, DiscoveredSchemaTable as m, CRM_CONCEPTS as n, ClivlyEntitiesConfigInput as o, DiscoveredColumnMeta as p, CUSTOM_CONCEPT as r, ClivlyEntityConfig as s, CANONICAL_FIELDS as t, ConfigValidationResult as u, MaterializingConcept as v, entitiesConfigSchema as w, RelationshipsMap as x, RelationshipSpec as y };
@@ -437,10 +437,23 @@ const entitySchema = require_projection.strictObject({
437
437
  relationships: require_projection.record(require_projection.string(), relationshipSchema).optional(),
438
438
  /**
439
439
  * For a "custom" entity, the crm_object_types row this materializes into.
440
- * Required for custom (enforced at the API layer), unused for standard concepts.
440
+ * Server-generated, so pasting one into config couples the file to a
441
+ * specific workspace. Prefer declaring `objectType` and letting
442
+ * `clivly push-schema` resolve the id by slug.
441
443
  */
442
444
  targetObjectTypeId: require_projection.string().min(1).optional(),
443
445
  /**
446
+ * Declares the custom object this entity materializes into, so config is
447
+ * self-contained: the entity key is the slug (unique per org and immutable),
448
+ * and `clivly push-schema` creates the object type if the workspace does not
449
+ * have it yet. An alternative to pasting a `targetObjectTypeId`.
450
+ */
451
+ objectType: require_projection.strictObject({
452
+ name: require_projection.string().min(1),
453
+ pluralName: require_projection.string().min(1).optional(),
454
+ iconKey: require_projection.string().min(1).optional()
455
+ }).optional(),
456
+ /**
444
457
  * Advanced mapping: derive this entity from a base table plus explicit joins
445
458
  * instead of a single source table. Valid only for `concept: "custom"`.
446
459
  *
@@ -482,19 +495,69 @@ function defaultWarn(message) {
482
495
  console.warn(message);
483
496
  }
484
497
  /**
485
- * Structural helper for `clivly.config.ts`. Gives full type inference on the
486
- * literal you pass and eagerly parses it, so shape errors surface at config
487
- * load rather than at sync time.
488
- *
489
- * It also catches the silent-data-loss footgun: a non-canonical field key for a
490
- * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
491
- * destination column, so the store would write an empty value. By default these
492
- * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
493
- * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
494
- * needs the discovered schema; see `validateEntitiesConfig`.
498
+ * The custom objects a config declares, keyed by entity slug. Entities that
499
+ * already carry an explicit `targetObjectTypeId` are omitted: their object type
500
+ * exists server-side and re-declaring it would be a no-op at best.
501
+ */
502
+ function customObjectDeclarations(config) {
503
+ const declarations = [];
504
+ for (const [slug, entity] of Object.entries(config.entities)) {
505
+ if (entity.concept !== "custom" || entity.targetObjectTypeId) continue;
506
+ if (!entity.objectType) continue;
507
+ declarations.push({
508
+ slug,
509
+ ...entity.objectType
510
+ });
511
+ }
512
+ return declarations;
513
+ }
514
+ /**
515
+ * The `fields` entry a projection binding implies, qualified as `ref.column`.
516
+ * Qualified rather than bare: two bindings can pull the same column name from
517
+ * different tables (`cl.full_name` and `adv.full_name`), which a bare column
518
+ * name cannot tell apart.
519
+ */
520
+ function projectionFields(projection) {
521
+ const fields = {};
522
+ for (const [name, binding] of Object.entries(projection.fieldBindings)) fields[name] = `${binding.from.table}.${binding.from.column}`;
523
+ return fields;
524
+ }
525
+ /**
526
+ * `fields` duplicates `projection.fieldBindings`, and only the bindings are
527
+ * compiled — so a hand-written `fields` that drifts is both silent and
528
+ * invisible. Derive it when omitted, and reject it when it disagrees.
495
529
  */
530
+ function normalizeProjectionFields(config) {
531
+ const entities = {};
532
+ for (const [key, entity] of Object.entries(config.entities ?? {})) {
533
+ const projection = entity?.projection;
534
+ if (!projection?.fieldBindings) {
535
+ entities[key] = entity;
536
+ continue;
537
+ }
538
+ const derived = projectionFields(projection);
539
+ const declared = entity.fields;
540
+ if (!declared) {
541
+ entities[key] = {
542
+ ...entity,
543
+ fields: derived
544
+ };
545
+ continue;
546
+ }
547
+ const drifted = [.../* @__PURE__ */ new Set([...Object.keys(derived), ...Object.keys(declared)])].filter((name) => declared[name] !== derived[name]).sort();
548
+ if (drifted.length > 0) {
549
+ 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("; ");
550
+ 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.`);
551
+ }
552
+ entities[key] = entity;
553
+ }
554
+ return {
555
+ ...config,
556
+ entities
557
+ };
558
+ }
496
559
  function defineClivlyConfig(config, options) {
497
- const parsed = entitiesConfigSchema.parse(config);
560
+ const parsed = entitiesConfigSchema.parse(normalizeProjectionFields(config));
498
561
  const issues = canonicalFieldIssues(parsed);
499
562
  if (issues.length > 0) {
500
563
  if (options?.strict) throw new ClivlyConfigError(issues);
@@ -587,7 +650,7 @@ function validateEntity(v, config, key, entity, schema) {
587
650
  }
588
651
  if (entity.projection) {
589
652
  if (entity.concept !== "custom") v.report(`${base}.projection`, `projection mappings are only supported for concept "${CUSTOM_CONCEPT}"`);
590
- if (!entity.targetObjectTypeId) v.report(`${base}.targetObjectTypeId`, "a projection mapping must target a custom object type");
653
+ 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`");
591
654
  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`);
592
655
  for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
593
656
  validateReadOnly(v, base, entity);
@@ -659,6 +722,12 @@ Object.defineProperty(exports, "coversUniqueKey", {
659
722
  return coversUniqueKey;
660
723
  }
661
724
  });
725
+ Object.defineProperty(exports, "customObjectDeclarations", {
726
+ enumerable: true,
727
+ get: function() {
728
+ return customObjectDeclarations;
729
+ }
730
+ });
662
731
  Object.defineProperty(exports, "defineClivlyConfig", {
663
732
  enumerable: true,
664
733
  get: function() {
@@ -701,6 +770,12 @@ Object.defineProperty(exports, "primaryKeyColumns", {
701
770
  return primaryKeyColumns;
702
771
  }
703
772
  });
773
+ Object.defineProperty(exports, "projectionFields", {
774
+ enumerable: true,
775
+ get: function() {
776
+ return projectionFields;
777
+ }
778
+ });
704
779
  Object.defineProperty(exports, "relationshipSchema", {
705
780
  enumerable: true,
706
781
  get: function() {
@@ -429,10 +429,23 @@ const entitySchema = strictObject({
429
429
  relationships: record(string(), relationshipSchema).optional(),
430
430
  /**
431
431
  * For a "custom" entity, the crm_object_types row this materializes into.
432
- * Required for custom (enforced at the API layer), unused for standard concepts.
432
+ * Server-generated, so pasting one into config couples the file to a
433
+ * specific workspace. Prefer declaring `objectType` and letting
434
+ * `clivly push-schema` resolve the id by slug.
433
435
  */
434
436
  targetObjectTypeId: string().min(1).optional(),
435
437
  /**
438
+ * Declares the custom object this entity materializes into, so config is
439
+ * self-contained: the entity key is the slug (unique per org and immutable),
440
+ * and `clivly push-schema` creates the object type if the workspace does not
441
+ * have it yet. An alternative to pasting a `targetObjectTypeId`.
442
+ */
443
+ objectType: strictObject({
444
+ name: string().min(1),
445
+ pluralName: string().min(1).optional(),
446
+ iconKey: string().min(1).optional()
447
+ }).optional(),
448
+ /**
436
449
  * Advanced mapping: derive this entity from a base table plus explicit joins
437
450
  * instead of a single source table. Valid only for `concept: "custom"`.
438
451
  *
@@ -474,19 +487,69 @@ function defaultWarn(message) {
474
487
  console.warn(message);
475
488
  }
476
489
  /**
477
- * Structural helper for `clivly.config.ts`. Gives full type inference on the
478
- * literal you pass and eagerly parses it, so shape errors surface at config
479
- * load rather than at sync time.
480
- *
481
- * It also catches the silent-data-loss footgun: a non-canonical field key for a
482
- * materialised concept (e.g. `fullName` instead of `name` on a `contact`) has no
483
- * destination column, so the store would write an empty value. By default these
484
- * are reported via `onWarn` (a `console.warn`); pass `{ strict: true }` to throw
485
- * a `ClivlyConfigError` instead. Does NOT validate against the host schema — that
486
- * needs the discovered schema; see `validateEntitiesConfig`.
490
+ * The custom objects a config declares, keyed by entity slug. Entities that
491
+ * already carry an explicit `targetObjectTypeId` are omitted: their object type
492
+ * exists server-side and re-declaring it would be a no-op at best.
493
+ */
494
+ function customObjectDeclarations(config) {
495
+ const declarations = [];
496
+ for (const [slug, entity] of Object.entries(config.entities)) {
497
+ if (entity.concept !== "custom" || entity.targetObjectTypeId) continue;
498
+ if (!entity.objectType) continue;
499
+ declarations.push({
500
+ slug,
501
+ ...entity.objectType
502
+ });
503
+ }
504
+ return declarations;
505
+ }
506
+ /**
507
+ * The `fields` entry a projection binding implies, qualified as `ref.column`.
508
+ * Qualified rather than bare: two bindings can pull the same column name from
509
+ * different tables (`cl.full_name` and `adv.full_name`), which a bare column
510
+ * name cannot tell apart.
511
+ */
512
+ function projectionFields(projection) {
513
+ const fields = {};
514
+ for (const [name, binding] of Object.entries(projection.fieldBindings)) fields[name] = `${binding.from.table}.${binding.from.column}`;
515
+ return fields;
516
+ }
517
+ /**
518
+ * `fields` duplicates `projection.fieldBindings`, and only the bindings are
519
+ * compiled — so a hand-written `fields` that drifts is both silent and
520
+ * invisible. Derive it when omitted, and reject it when it disagrees.
487
521
  */
522
+ function normalizeProjectionFields(config) {
523
+ const entities = {};
524
+ for (const [key, entity] of Object.entries(config.entities ?? {})) {
525
+ const projection = entity?.projection;
526
+ if (!projection?.fieldBindings) {
527
+ entities[key] = entity;
528
+ continue;
529
+ }
530
+ const derived = projectionFields(projection);
531
+ const declared = entity.fields;
532
+ if (!declared) {
533
+ entities[key] = {
534
+ ...entity,
535
+ fields: derived
536
+ };
537
+ continue;
538
+ }
539
+ const drifted = [.../* @__PURE__ */ new Set([...Object.keys(derived), ...Object.keys(declared)])].filter((name) => declared[name] !== derived[name]).sort();
540
+ if (drifted.length > 0) {
541
+ 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("; ");
542
+ 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.`);
543
+ }
544
+ entities[key] = entity;
545
+ }
546
+ return {
547
+ ...config,
548
+ entities
549
+ };
550
+ }
488
551
  function defineClivlyConfig(config, options) {
489
- const parsed = entitiesConfigSchema.parse(config);
552
+ const parsed = entitiesConfigSchema.parse(normalizeProjectionFields(config));
490
553
  const issues = canonicalFieldIssues(parsed);
491
554
  if (issues.length > 0) {
492
555
  if (options?.strict) throw new ClivlyConfigError(issues);
@@ -579,7 +642,7 @@ function validateEntity(v, config, key, entity, schema) {
579
642
  }
580
643
  if (entity.projection) {
581
644
  if (entity.concept !== "custom") v.report(`${base}.projection`, `projection mappings are only supported for concept "${CUSTOM_CONCEPT}"`);
582
- if (!entity.targetObjectTypeId) v.report(`${base}.targetObjectTypeId`, "a projection mapping must target a custom object type");
645
+ 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`");
583
646
  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`);
584
647
  for (const error of validateProjection(entity.projection, schema, base)) v.report(error.path, error.message);
585
648
  validateReadOnly(v, base, entity);
@@ -609,4 +672,4 @@ function validateEntitiesConfig(config, schema) {
609
672
  };
610
673
  }
611
674
  //#endregion
612
- 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 };
675
+ 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 };