@clivly/core 0.11.0-next.0 → 0.11.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.
Files changed (43) hide show
  1. package/dist/adapter.d.cts +44 -7
  2. package/dist/adapter.d.mts +44 -7
  3. package/dist/auth-adapter.d.cts +6 -7
  4. package/dist/auth-adapter.d.mts +6 -7
  5. package/dist/builder.d.cts +12 -12
  6. package/dist/builder.d.mts +12 -12
  7. package/dist/drizzle.d.cts +5 -6
  8. package/dist/drizzle.d.mts +5 -6
  9. package/dist/{entity-config-BTAJe1Zj.mjs → entity-config-C0rtu-K_.mjs} +19 -2
  10. package/dist/{entity-config-CqJknGOn.cjs → entity-config-C2sKAti2.cjs} +19 -2
  11. package/dist/{entity-config-Ddgukq_E.d.cts → entity-config-OhC4Otkz.d.cts} +3 -1
  12. package/dist/{entity-config-Ddgukq_E.d.mts → entity-config-OhC4Otkz.d.mts} +3 -1
  13. package/dist/entity-config.cjs +1 -1
  14. package/dist/entity-config.d.cts +1 -1
  15. package/dist/entity-config.d.mts +1 -1
  16. package/dist/entity-config.mjs +1 -1
  17. package/dist/entity-heuristics.cjs +9 -7
  18. package/dist/entity-heuristics.d.cts +21 -21
  19. package/dist/entity-heuristics.d.mts +21 -21
  20. package/dist/entity-heuristics.mjs +9 -7
  21. package/dist/env-file.d.cts +6 -7
  22. package/dist/env-file.d.mts +6 -7
  23. package/dist/index.cjs +83 -1
  24. package/dist/index.d.cts +89 -22
  25. package/dist/index.d.mts +89 -22
  26. package/dist/index.mjs +80 -2
  27. package/dist/integration-state.d.cts +30 -31
  28. package/dist/integration-state.d.mts +30 -31
  29. package/dist/mapping-form.d.cts +6 -7
  30. package/dist/mapping-form.d.mts +6 -7
  31. package/dist/mapping-score.d.cts +14 -15
  32. package/dist/mapping-score.d.mts +14 -15
  33. package/dist/mappings-config.cjs +2 -1
  34. package/dist/mappings-config.d.cts +8 -7
  35. package/dist/mappings-config.d.mts +8 -7
  36. package/dist/mappings-config.mjs +2 -1
  37. package/dist/sync-engine.d.cts +15 -16
  38. package/dist/sync-engine.d.mts +15 -16
  39. package/dist/types.d.cts +96 -33
  40. package/dist/types.d.mts +96 -33
  41. package/dist/view-compiler.d.cts +10 -11
  42. package/dist/view-compiler.d.mts +10 -11
  43. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-CqJknGOn.cjs");
2
+ const require_entity_config = require("./entity-config-C2sKAti2.cjs");
3
3
  const require_mapping_score = require("./mapping-score.cjs");
4
4
  //#region src/entity-heuristics.ts
5
5
  const TARGET_CONCEPTS = [
@@ -379,12 +379,14 @@ function scoreConcepts(table) {
379
379
  score += require_mapping_score.W.hasPrimaryKey;
380
380
  reasons.push("has primary key");
381
381
  }
382
- if (table.rowCount !== void 0) if (table.rowCount === 0) {
383
- score += require_mapping_score.W.emptyTable;
384
- reasons.push("table is empty");
385
- } else {
386
- score += require_mapping_score.W.hasRows;
387
- reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
382
+ if (table.rowCount !== void 0) {
383
+ if (table.rowCount === 0) {
384
+ score += require_mapping_score.W.emptyTable;
385
+ reasons.push("table is empty");
386
+ } else {
387
+ score += require_mapping_score.W.hasRows;
388
+ reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
389
+ }
388
390
  }
389
391
  out.push({
390
392
  value: concept,
@@ -1,13 +1,13 @@
1
- import { p as DiscoveredColumnMeta } from "./entity-config-Ddgukq_E.cjs";
1
+ import { p as DiscoveredColumnMeta } from "./entity-config-OhC4Otkz.cjs";
2
2
  import { Scored } from "./mapping-score.cjs";
3
3
  //#region src/entity-heuristics.d.ts
4
- declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
5
- type TargetConcept = (typeof TARGET_CONCEPTS)[number];
6
- declare function suggestCursorField(columnKeys: string[]): string | null;
7
- declare function matchesConcept(tableName: string, concept: TargetConcept): {
4
+ export declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
5
+ export type TargetConcept = (typeof TARGET_CONCEPTS)[number];
6
+ export declare function suggestCursorField(columnKeys: string[]): string | null;
7
+ export declare function matchesConcept(tableName: string, concept: TargetConcept): {
8
8
  exact: boolean;
9
9
  } | null;
10
- interface ScoredTable {
10
+ export interface ScoredTable {
11
11
  columns: string[];
12
12
  columnsMeta?: DiscoveredColumnMeta[];
13
13
  name: string;
@@ -18,7 +18,7 @@ interface ScoredTable {
18
18
  * and from bulk apply, so one click can never map an auth table into the CRM.
19
19
  * They stay manually selectable — an explicit, informed choice is legitimate.
20
20
  */
21
- declare function hasSensitiveColumns(columns: string[]): boolean;
21
+ export declare function hasSensitiveColumns(columns: string[]): boolean;
22
22
  /**
23
23
  * Is this table one your auth library created, rather than one you wrote?
24
24
  *
@@ -26,20 +26,20 @@ declare function hasSensitiveColumns(columns: string[]): boolean;
26
26
  * concept. Never to exclude them: mapping `user` as the contact entity is
27
27
  * legitimate for plenty of apps, and it stays selectable.
28
28
  */
29
- declare function isFrameworkTable(name: string): boolean;
30
- declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
31
- declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
32
- declare function topConcept(table: ScoredTable): {
29
+ export declare function isFrameworkTable(name: string): boolean;
30
+ export declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
31
+ export declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
32
+ export declare function topConcept(table: ScoredTable): {
33
33
  best: Scored<TargetConcept>;
34
34
  ambiguous: boolean;
35
35
  } | null;
36
- declare function requiredFieldsFor(concept: TargetConcept | "custom"): string[];
37
- declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
36
+ export declare function requiredFieldsFor(concept: TargetConcept | "custom"): string[];
37
+ export declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
38
38
  valid: boolean;
39
39
  missing: string[];
40
40
  };
41
- declare function suggestDiscriminatorColumns(columns: string[]): string[];
42
- type SuggestedVia = {
41
+ export declare function suggestDiscriminatorColumns(columns: string[]): string[];
42
+ export type SuggestedVia = {
43
43
  fkOn: "company" | "contact";
44
44
  column: string;
45
45
  } | {
@@ -47,7 +47,7 @@ type SuggestedVia = {
47
47
  localKey: string;
48
48
  foreignKey: string;
49
49
  };
50
- interface RelationshipSuggestion {
50
+ export interface RelationshipSuggestion {
51
51
  /** The related host table. */
52
52
  entityTable: string;
53
53
  /** Suggested relationship name (defaults to the related concept). */
@@ -60,13 +60,13 @@ interface RelationshipSuggestion {
60
60
  * that scores highest. Without metadata (v1 hosts) we fall back to the legacy
61
61
  * FK-name hints at a lower tier.
62
62
  */
63
- declare function scoreRelationships(source: ScoredTable, allTables: ScoredTable[]): Scored<RelationshipSuggestion>[];
64
- interface FieldSuggestion {
63
+ export declare function scoreRelationships(source: ScoredTable, allTables: ScoredTable[]): Scored<RelationshipSuggestion>[];
64
+ export interface FieldSuggestion {
65
65
  column: string;
66
66
  field: string;
67
67
  required: boolean;
68
68
  }
69
- declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
70
- declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
69
+ export declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
70
+ export declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
71
71
  //#endregion
72
- export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
72
+ export type { DiscoveredColumnMeta };
@@ -1,13 +1,13 @@
1
- import { p as DiscoveredColumnMeta } from "./entity-config-Ddgukq_E.mjs";
1
+ import { p as DiscoveredColumnMeta } from "./entity-config-OhC4Otkz.mjs";
2
2
  import { Scored } from "./mapping-score.mjs";
3
3
  //#region src/entity-heuristics.d.ts
4
- declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
5
- type TargetConcept = (typeof TARGET_CONCEPTS)[number];
6
- declare function suggestCursorField(columnKeys: string[]): string | null;
7
- declare function matchesConcept(tableName: string, concept: TargetConcept): {
4
+ export declare const TARGET_CONCEPTS: readonly ["contact", "company", "deal", "activity", "conversation", "note"];
5
+ export type TargetConcept = (typeof TARGET_CONCEPTS)[number];
6
+ export declare function suggestCursorField(columnKeys: string[]): string | null;
7
+ export declare function matchesConcept(tableName: string, concept: TargetConcept): {
8
8
  exact: boolean;
9
9
  } | null;
10
- interface ScoredTable {
10
+ export interface ScoredTable {
11
11
  columns: string[];
12
12
  columnsMeta?: DiscoveredColumnMeta[];
13
13
  name: string;
@@ -18,7 +18,7 @@ interface ScoredTable {
18
18
  * and from bulk apply, so one click can never map an auth table into the CRM.
19
19
  * They stay manually selectable — an explicit, informed choice is legitimate.
20
20
  */
21
- declare function hasSensitiveColumns(columns: string[]): boolean;
21
+ export declare function hasSensitiveColumns(columns: string[]): boolean;
22
22
  /**
23
23
  * Is this table one your auth library created, rather than one you wrote?
24
24
  *
@@ -26,20 +26,20 @@ declare function hasSensitiveColumns(columns: string[]): boolean;
26
26
  * concept. Never to exclude them: mapping `user` as the contact entity is
27
27
  * legitimate for plenty of apps, and it stays selectable.
28
28
  */
29
- declare function isFrameworkTable(name: string): boolean;
30
- declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
31
- declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
32
- declare function topConcept(table: ScoredTable): {
29
+ export declare function isFrameworkTable(name: string): boolean;
30
+ export declare function hasDefiningColumn(concept: TargetConcept, columns: string[]): boolean;
31
+ export declare function scoreConcepts(table: ScoredTable): Scored<TargetConcept>[];
32
+ export declare function topConcept(table: ScoredTable): {
33
33
  best: Scored<TargetConcept>;
34
34
  ambiguous: boolean;
35
35
  } | null;
36
- declare function requiredFieldsFor(concept: TargetConcept | "custom"): string[];
37
- declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
36
+ export declare function requiredFieldsFor(concept: TargetConcept | "custom"): string[];
37
+ export declare function validateMapping(requiredFields: string[], fieldMap: Record<string, string>): {
38
38
  valid: boolean;
39
39
  missing: string[];
40
40
  };
41
- declare function suggestDiscriminatorColumns(columns: string[]): string[];
42
- type SuggestedVia = {
41
+ export declare function suggestDiscriminatorColumns(columns: string[]): string[];
42
+ export type SuggestedVia = {
43
43
  fkOn: "company" | "contact";
44
44
  column: string;
45
45
  } | {
@@ -47,7 +47,7 @@ type SuggestedVia = {
47
47
  localKey: string;
48
48
  foreignKey: string;
49
49
  };
50
- interface RelationshipSuggestion {
50
+ export interface RelationshipSuggestion {
51
51
  /** The related host table. */
52
52
  entityTable: string;
53
53
  /** Suggested relationship name (defaults to the related concept). */
@@ -60,13 +60,13 @@ interface RelationshipSuggestion {
60
60
  * that scores highest. Without metadata (v1 hosts) we fall back to the legacy
61
61
  * FK-name hints at a lower tier.
62
62
  */
63
- declare function scoreRelationships(source: ScoredTable, allTables: ScoredTable[]): Scored<RelationshipSuggestion>[];
64
- interface FieldSuggestion {
63
+ export declare function scoreRelationships(source: ScoredTable, allTables: ScoredTable[]): Scored<RelationshipSuggestion>[];
64
+ export interface FieldSuggestion {
65
65
  column: string;
66
66
  field: string;
67
67
  required: boolean;
68
68
  }
69
- declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
70
- declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
69
+ export declare function scoreFieldMap(concept: TargetConcept, table: ScoredTable): Scored<FieldSuggestion>[];
70
+ export declare function toFieldMap(scored: Scored<FieldSuggestion>[]): Record<string, string>;
71
71
  //#endregion
72
- export { type DiscoveredColumnMeta, FieldSuggestion, RelationshipSuggestion, ScoredTable, SuggestedVia, TARGET_CONCEPTS, TargetConcept, hasDefiningColumn, hasSensitiveColumns, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping };
72
+ export type { DiscoveredColumnMeta };
@@ -1,4 +1,4 @@
1
- import { a as MATERIALIZING_CONCEPTS } from "./entity-config-BTAJe1Zj.mjs";
1
+ import { a as MATERIALIZING_CONCEPTS } from "./entity-config-C0rtu-K_.mjs";
2
2
  import { CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor } from "./mapping-score.mjs";
3
3
  //#region src/entity-heuristics.ts
4
4
  const TARGET_CONCEPTS = [
@@ -378,12 +378,14 @@ function scoreConcepts(table) {
378
378
  score += W.hasPrimaryKey;
379
379
  reasons.push("has primary key");
380
380
  }
381
- if (table.rowCount !== void 0) if (table.rowCount === 0) {
382
- score += W.emptyTable;
383
- reasons.push("table is empty");
384
- } else {
385
- score += W.hasRows;
386
- reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
381
+ if (table.rowCount !== void 0) {
382
+ if (table.rowCount === 0) {
383
+ score += W.emptyTable;
384
+ reasons.push("table is empty");
385
+ } else {
386
+ score += W.hasRows;
387
+ reasons.push(`${table.rowCount.toLocaleString("en-US")} rows`);
388
+ }
387
389
  }
388
390
  out.push({
389
391
  value: concept,
@@ -1,10 +1,10 @@
1
1
  //#region src/env-file.d.ts
2
- declare const ENV_FRAMEWORKS: readonly ["next", "tanstack-start", "sveltekit", "remix", "nuxt", "hono-workers"];
3
- type EnvFramework = (typeof ENV_FRAMEWORKS)[number];
2
+ export declare const ENV_FRAMEWORKS: readonly ["next", "tanstack-start", "sveltekit", "remix", "nuxt", "hono-workers"];
3
+ export type EnvFramework = (typeof ENV_FRAMEWORKS)[number];
4
4
  /** The conventional env file for a framework. `.env` when we do not know. */
5
- declare function envFileFor(framework: string | null | undefined): string;
5
+ export declare function envFileFor(framework: string | null | undefined): string;
6
6
  /** The canonical id for a framework name, or null when unrecognised. */
7
- declare function normalizeFramework(framework: string | null | undefined): EnvFramework | null;
7
+ export declare function normalizeFramework(framework: string | null | undefined): EnvFramework | null;
8
8
  /**
9
9
  * Which env file to actually write to.
10
10
  *
@@ -14,10 +14,9 @@ declare function normalizeFramework(framework: string | null | undefined): EnvFr
14
14
  * stale one still loading. Convention applies to new setups, not to a working
15
15
  * one someone already has.
16
16
  */
17
- declare function resolveEnvFile(input: {
17
+ export declare function resolveEnvFile(input: {
18
18
  /** Candidate filenames that already contain the credential, if any. */
19
19
  existing: string[];
20
20
  framework: string | null | undefined;
21
21
  }): string;
22
- //#endregion
23
- export { ENV_FRAMEWORKS, EnvFramework, envFileFor, normalizeFramework, resolveEnvFile };
22
+ //#endregion
@@ -1,10 +1,10 @@
1
1
  //#region src/env-file.d.ts
2
- declare const ENV_FRAMEWORKS: readonly ["next", "tanstack-start", "sveltekit", "remix", "nuxt", "hono-workers"];
3
- type EnvFramework = (typeof ENV_FRAMEWORKS)[number];
2
+ export declare const ENV_FRAMEWORKS: readonly ["next", "tanstack-start", "sveltekit", "remix", "nuxt", "hono-workers"];
3
+ export type EnvFramework = (typeof ENV_FRAMEWORKS)[number];
4
4
  /** The conventional env file for a framework. `.env` when we do not know. */
5
- declare function envFileFor(framework: string | null | undefined): string;
5
+ export declare function envFileFor(framework: string | null | undefined): string;
6
6
  /** The canonical id for a framework name, or null when unrecognised. */
7
- declare function normalizeFramework(framework: string | null | undefined): EnvFramework | null;
7
+ export declare function normalizeFramework(framework: string | null | undefined): EnvFramework | null;
8
8
  /**
9
9
  * Which env file to actually write to.
10
10
  *
@@ -14,10 +14,9 @@ declare function normalizeFramework(framework: string | null | undefined): EnvFr
14
14
  * stale one still loading. Convention applies to new setups, not to a working
15
15
  * one someone already has.
16
16
  */
17
- declare function resolveEnvFile(input: {
17
+ export declare function resolveEnvFile(input: {
18
18
  /** Candidate filenames that already contain the credential, if any. */
19
19
  existing: string[];
20
20
  framework: string | null | undefined;
21
21
  }): string;
22
- //#endregion
23
- export { ENV_FRAMEWORKS, EnvFramework, envFileFor, normalizeFramework, resolveEnvFile };
22
+ //#endregion
package/dist/index.cjs CHANGED
@@ -2,12 +2,90 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_auth_adapter = require("./auth-adapter.cjs");
3
3
  const require_projection = require("./projection-DFgCoyZl.cjs");
4
4
  const require_schema_meta = require("./schema-meta-Cgi0MnHh.cjs");
5
- const require_entity_config = require("./entity-config-CqJknGOn.cjs");
5
+ const require_entity_config = require("./entity-config-C2sKAti2.cjs");
6
6
  const require_mapping_form = require("./mapping-form.cjs");
7
7
  const require_mappings_config = require("./mappings-config.cjs");
8
8
  const require_view_compiler = require("./view-compiler-DP4Fbj7-.cjs");
9
9
  const require_sync_engine = require("./sync-engine.cjs");
10
10
  const require_types = require("./types.cjs");
11
+ //#region src/host-owned-fields.ts
12
+ /**
13
+ * Which fields a CRM record lets a user edit.
14
+ *
15
+ * A record synced from the host app carries a `sourceRef`. Its identity fields
16
+ * are rewritten from the host view on every sync run, so an edit to one would
17
+ * be silently reverted minutes later — the user sees their change accepted,
18
+ * then gone, with nothing explaining why. That is worse than refusing it.
19
+ *
20
+ * So host-owned fields are rejected at the API, not merely disabled in the UI:
21
+ * the UI is not the security boundary, and the SDK and any future public API
22
+ * reach the same routers.
23
+ *
24
+ * Records created inside the CRM have no `sourceRef` and are fully editable —
25
+ * nothing else owns them.
26
+ *
27
+ * This is the cheap half of a capability matrix. It answers "may a user write
28
+ * this?" but not "may the sync engine write this?", which stays implicit
29
+ * because sync bypasses these routers entirely. An override model (the
30
+ * `stageOverrideAt` pattern on `crm_deals`) can replace this later without
31
+ * changing callers.
32
+ */
33
+ /** Contact fields the host view owns when the row is synced. */
34
+ const HOST_OWNED_CONTACT_FIELDS = ["email", "name"];
35
+ /**
36
+ * Company fields the host view owns when the row is synced.
37
+ *
38
+ * `email` and `phone` joined this list in the same change that made them
39
+ * mappable (migration 0048). They are not identity fields, but ownership here
40
+ * follows what sync rewrites, not what feels identity-like: both flow from the
41
+ * host view on every run, exactly as `email` does for contacts above. Widening
42
+ * `CANONICAL_FIELDS.company` without widening this list is the specific bug
43
+ * this module's doc comment calls worse than refusing the edit.
44
+ *
45
+ * `primaryContactId` is deliberately absent: it is a CRM-side judgement about
46
+ * who to talk to, never sourced from the host, so it stays editable on synced
47
+ * rows.
48
+ */
49
+ const HOST_OWNED_COMPANY_FIELDS = [
50
+ "domain",
51
+ "name",
52
+ "email",
53
+ "phone"
54
+ ];
55
+ /**
56
+ * True when the record is owned by a host view rather than by the CRM.
57
+ *
58
+ * Deliberately keyed on `sourceRef` and not `entityRole`: `entityRole` is a
59
+ * mapping label that can be absent on a synced row, while `sourceRef` is the
60
+ * identity the sync engine reconciles against and is set for exactly the rows
61
+ * sync will rewrite.
62
+ */
63
+ function isHostOwned(record) {
64
+ return Boolean(record.sourceRef);
65
+ }
66
+ function rejectedFields(record, patch, owned) {
67
+ if (!isHostOwned(record)) return [];
68
+ return owned.filter((field) => patch[field] !== void 0);
69
+ }
70
+ /**
71
+ * Host-owned contact fields this patch would change, or `[]` when the patch is
72
+ * allowed. Callers turn a non-empty result into a 400 naming the fields.
73
+ */
74
+ function rejectedContactFields(contact, patch) {
75
+ return rejectedFields(contact, patch, HOST_OWNED_CONTACT_FIELDS);
76
+ }
77
+ /** Host-owned company fields this patch would change, or `[]`. */
78
+ function rejectedCompanyFields(company, patch) {
79
+ return rejectedFields(company, patch, HOST_OWNED_COMPANY_FIELDS);
80
+ }
81
+ /**
82
+ * The message shown when a write is refused. Names the fields and says where
83
+ * they come from, so the reason is actionable rather than a bare "forbidden".
84
+ */
85
+ function hostOwnedFieldMessage(fields) {
86
+ return `The ${fields.join(", ")} ${fields.length > 1 ? "fields are" : "field is"} synced from your app and cannot be edited here. Change it in your app and it will update on the next sync.`;
87
+ }
88
+ //#endregion
11
89
  //#region src/slug.ts
12
90
  /**
13
91
  * The one `slugify`.
@@ -180,8 +258,10 @@ exports.explainEntitiesConfig = require_view_compiler.explainEntitiesConfig;
180
258
  exports.fanOutJoins = require_entity_config.fanOutJoins;
181
259
  exports.filterSchema = require_entity_config.filterSchema;
182
260
  exports.hasRichMeta = require_schema_meta.hasRichMeta;
261
+ exports.hostOwnedFieldMessage = hostOwnedFieldMessage;
183
262
  exports.identityRefs = require_projection.identityRefs;
184
263
  exports.isClosedStageType = require_types.isClosedStageType;
264
+ exports.isHostOwned = isHostOwned;
185
265
  exports.joinFansOut = require_entity_config.joinFansOut;
186
266
  exports.mappingsToEntitiesConfig = require_mappings_config.mappingsToEntitiesConfig;
187
267
  exports.matchesCondition = matchesCondition;
@@ -191,6 +271,8 @@ exports.projectionFields = require_entity_config.projectionFields;
191
271
  exports.projectionSchema = require_projection.projectionSchema;
192
272
  exports.quoteIdent = require_view_compiler.quoteIdent;
193
273
  exports.reconcile = require_sync_engine.reconcile;
274
+ exports.rejectedCompanyFields = rejectedCompanyFields;
275
+ exports.rejectedContactFields = rejectedContactFields;
194
276
  exports.relationshipSchema = require_entity_config.relationshipSchema;
195
277
  exports.relationshipViaSignature = require_mapping_form.relationshipViaSignature;
196
278
  exports.relationshipsSchema = require_entity_config.relationshipsSchema;
package/dist/index.d.cts CHANGED
@@ -1,16 +1,83 @@
1
- import { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, OPEN_STAGE_TYPE, PipelineConfig, PipelineKind, PipelineStageConfig, RecordSource, StageType, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput, ValueInterval, isClosedStageType } from "./types.cjs";
1
+ import { Activity, ActivityType, Company, CompanyFilters, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, OPEN_STAGE_TYPE, PipelineConfig, PipelineKind, PipelineStageConfig, RecordSource, StageType, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateCompanyInput, UpdateContactInput, UpdateDealInput, UpdateTaskInput, ValueInterval, isClosedStageType } from "./types.cjs";
2
2
  import { CRMAdapter } from "./adapter.cjs";
3
3
  import { ClivlyAuthAdapter, ClivlyUser, CrmMembership, MembershipResolver, authCustom } from "./auth-adapter.cjs";
4
- import { A as relationshipsSchema, B as identityRefs, D as missingRequiredFields, E as filterSchema, F as ProjectionFieldBinding, I as ProjectionIdentity, L as ProjectionJoin, M as validateEntitiesConfig, N as BASE_REF, O as projectionFields, P as PROJECTION_KEY_PATTERN, R as ProjectionMapping, S as RelationshipsMap, T as entitiesConfigSchema, V as projectionSchema, _ as MATERIALIZING_CONCEPTS, a as ClivlyEntitiesConfig, b as RelationshipSpec, d as CrmConcept, g as FilterValue, h as FilterExpr, i as ClivlyConfigError, j as requiredFieldsFor, k as relationshipSchema, l as ConfigValidationError, m as DiscoveredSchemaTable, n as CRM_CONCEPTS, p as DiscoveredColumnMeta, s as ClivlyEntityConfig, t as CANONICAL_FIELDS, u as ConfigValidationResult, v as MaterializingConcept, w as defineClivlyConfig, x as RelationshipVia, y as REQUIRED_FIELDS, z as ProjectionRef } from "./entity-config-Ddgukq_E.cjs";
4
+ import { A as relationshipsSchema, B as identityRefs, D as missingRequiredFields, E as filterSchema, F as ProjectionFieldBinding, I as ProjectionIdentity, L as ProjectionJoin, M as validateEntitiesConfig, N as BASE_REF, O as projectionFields, P as PROJECTION_KEY_PATTERN, R as ProjectionMapping, S as RelationshipsMap, T as entitiesConfigSchema, V as projectionSchema, _ as MATERIALIZING_CONCEPTS, a as ClivlyEntitiesConfig, b as RelationshipSpec, d as CrmConcept, g as FilterValue, h as FilterExpr, i as ClivlyConfigError, j as requiredFieldsFor, k as relationshipSchema, l as ConfigValidationError, m as DiscoveredSchemaTable, n as CRM_CONCEPTS, p as DiscoveredColumnMeta, s as ClivlyEntityConfig, t as CANONICAL_FIELDS, u as ConfigValidationResult, v as MaterializingConcept, w as defineClivlyConfig, x as RelationshipVia, y as REQUIRED_FIELDS, z as ProjectionRef } from "./entity-config-OhC4Otkz.cjs";
5
5
  import { ConnectionLiveness, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, StepId, StepProblem, StepState } from "./integration-state.cjs";
6
6
  import { RelationshipCandidate, buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.cjs";
7
7
  import { EntityMappingRow, mappingsToEntitiesConfig } from "./mappings-config.cjs";
8
8
  import { CompileOptions, CompiledView, DEFAULT_VIEW_PREFIX, ExplainResult, compileEntityView, compileEntityViews, explainEntitiesConfig } from "./view-compiler.cjs";
9
9
  import { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync } from "./sync-engine.cjs";
10
+ //#region src/host-owned-fields.d.ts
11
+ /**
12
+ * Which fields a CRM record lets a user edit.
13
+ *
14
+ * A record synced from the host app carries a `sourceRef`. Its identity fields
15
+ * are rewritten from the host view on every sync run, so an edit to one would
16
+ * be silently reverted minutes later — the user sees their change accepted,
17
+ * then gone, with nothing explaining why. That is worse than refusing it.
18
+ *
19
+ * So host-owned fields are rejected at the API, not merely disabled in the UI:
20
+ * the UI is not the security boundary, and the SDK and any future public API
21
+ * reach the same routers.
22
+ *
23
+ * Records created inside the CRM have no `sourceRef` and are fully editable —
24
+ * nothing else owns them.
25
+ *
26
+ * This is the cheap half of a capability matrix. It answers "may a user write
27
+ * this?" but not "may the sync engine write this?", which stays implicit
28
+ * because sync bypasses these routers entirely. An override model (the
29
+ * `stageOverrideAt` pattern on `crm_deals`) can replace this later without
30
+ * changing callers.
31
+ */
32
+ /** Contact fields the host view owns when the row is synced. */
33
+ declare const HOST_OWNED_CONTACT_FIELDS: readonly ["email", "name"];
34
+ /**
35
+ * Company fields the host view owns when the row is synced.
36
+ *
37
+ * `email` and `phone` joined this list in the same change that made them
38
+ * mappable (migration 0048). They are not identity fields, but ownership here
39
+ * follows what sync rewrites, not what feels identity-like: both flow from the
40
+ * host view on every run, exactly as `email` does for contacts above. Widening
41
+ * `CANONICAL_FIELDS.company` without widening this list is the specific bug
42
+ * this module's doc comment calls worse than refusing the edit.
43
+ *
44
+ * `primaryContactId` is deliberately absent: it is a CRM-side judgement about
45
+ * who to talk to, never sourced from the host, so it stays editable on synced
46
+ * rows.
47
+ */
48
+ declare const HOST_OWNED_COMPANY_FIELDS: readonly ["domain", "name", "email", "phone"];
49
+ type HostOwnedContactField = (typeof HOST_OWNED_CONTACT_FIELDS)[number];
50
+ type HostOwnedCompanyField = (typeof HOST_OWNED_COMPANY_FIELDS)[number];
51
+ /** The subset of a record this module needs to make its decision. */
52
+ interface SourcedRecord {
53
+ sourceRef?: string | null;
54
+ }
55
+ /**
56
+ * True when the record is owned by a host view rather than by the CRM.
57
+ *
58
+ * Deliberately keyed on `sourceRef` and not `entityRole`: `entityRole` is a
59
+ * mapping label that can be absent on a synced row, while `sourceRef` is the
60
+ * identity the sync engine reconciles against and is set for exactly the rows
61
+ * sync will rewrite.
62
+ */
63
+ export declare function isHostOwned(record: SourcedRecord): boolean;
64
+ /**
65
+ * Host-owned contact fields this patch would change, or `[]` when the patch is
66
+ * allowed. Callers turn a non-empty result into a 400 naming the fields.
67
+ */
68
+ export declare function rejectedContactFields(contact: SourcedRecord, patch: Readonly<Record<string, unknown>>): HostOwnedContactField[];
69
+ /** Host-owned company fields this patch would change, or `[]`. */
70
+ export declare function rejectedCompanyFields(company: SourcedRecord, patch: Readonly<Record<string, unknown>>): HostOwnedCompanyField[];
71
+ /**
72
+ * The message shown when a write is refused. Names the fields and says where
73
+ * they come from, so the reason is actionable rather than a bare "forbidden".
74
+ */
75
+ export declare function hostOwnedFieldMessage(fields: readonly string[]): string;
76
+ //#endregion
10
77
  //#region src/projection-cardinality.d.ts
11
- declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
78
+ export declare function joinFansOut(join: ProjectionJoin, joinTable: DiscoveredSchemaTable): boolean;
12
79
  /** Every join in the projection that can multiply rows. */
13
- declare function fanOutJoins(projection: ProjectionMapping, tablesByName: Map<string, DiscoveredSchemaTable>): ProjectionJoin[];
80
+ export declare function fanOutJoins(projection: ProjectionMapping, tablesByName: Map<string, DiscoveredSchemaTable>): ProjectionJoin[];
14
81
  //#endregion
15
82
  //#region src/projection-sql.d.ts
16
83
  /**
@@ -24,7 +91,7 @@ declare function fanOutJoins(projection: ProjectionMapping, tablesByName: Map<st
24
91
  * deterministic and injection-safe.
25
92
  */
26
93
  /** Quote a single identifier part, escaping embedded double-quotes. */
27
- declare function quoteIdent(name: string): string;
94
+ export declare function quoteIdent(name: string): string;
28
95
  /**
29
96
  * The identity expression. Composite identities concatenate their parts with
30
97
  * `::`; validation guarantees every part is NOT NULL and non-text, which is
@@ -35,14 +102,14 @@ declare function quoteIdent(name: string): string;
35
102
  * also orders and keyset-paginates by it. Sharing one definition is what keeps
36
103
  * a pushed row's `id` identical to the one the view would have produced.
37
104
  */
38
- declare function compileProjectionIdentity(projection: ProjectionMapping, basePkColumn: string): string;
39
- declare function compileProjectionSelect(projection: ProjectionMapping, basePkColumn: string): {
105
+ export declare function compileProjectionIdentity(projection: ProjectionMapping, basePkColumn: string): string;
106
+ export declare function compileProjectionSelect(projection: ProjectionMapping, basePkColumn: string): {
40
107
  columns: string[];
41
108
  selectItems: string[];
42
109
  };
43
- declare function compileProjectionJoins(projection: ProjectionMapping): string[];
110
+ export declare function compileProjectionJoins(projection: ProjectionMapping): string[];
44
111
  /** `FROM <baseTable> AS "base"` — the alias every ref resolves against. */
45
- declare function compileProjectionFrom(projection: ProjectionMapping): string;
112
+ export declare function compileProjectionFrom(projection: ProjectionMapping): string;
46
113
  /**
47
114
  * Hard ceiling on preview rows, regardless of what a caller asks for.
48
115
  *
@@ -50,7 +117,7 @@ declare function compileProjectionFrom(projection: ProjectionMapping): string;
50
117
  * as often as they like, so the cap belongs beside the query rather than in
51
118
  * whichever caller happens to be passing a limit today.
52
119
  */
53
- declare const PREVIEW_MAX_ROWS = 50;
120
+ export declare const PREVIEW_MAX_ROWS = 50;
54
121
  /**
55
122
  * One SELECT reading a few rows through an UNSAVED projection, for the derived
56
123
  * object builder's preview.
@@ -66,7 +133,7 @@ declare const PREVIEW_MAX_ROWS = 50;
66
133
  * reads `row[token]`; a preview is read by a person, in a table whose headers
67
134
  * are the field names they just typed.
68
135
  */
69
- declare function compileProjectionPreview(projection: ProjectionMapping, limit: number): string;
136
+ export declare function compileProjectionPreview(projection: ProjectionMapping, limit: number): string;
70
137
  //#endregion
71
138
  //#region src/projection-validate.d.ts
72
139
  /**
@@ -89,12 +156,12 @@ declare function compileProjectionPreview(projection: ProjectionMapping, limit:
89
156
  *
90
157
  * Rejecting an unresolvable column closes the last two together.
91
158
  */
92
- declare function validateProjectionFilter(filter: FilterExpr | undefined, projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
159
+ export declare function validateProjectionFilter(filter: FilterExpr | undefined, projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
93
160
  /**
94
161
  * Validate a projection against the discovered schema. Returns every error
95
162
  * found (not just the first) so a builder UI can show them all at once.
96
163
  */
97
- declare function validateProjection(projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
164
+ export declare function validateProjection(projection: ProjectionMapping, schema: DiscoveredSchemaTable[], base: string): ConfigValidationError[];
98
165
  //#endregion
99
166
  //#region src/schema-meta.d.ts
100
167
  /**
@@ -105,12 +172,12 @@ declare function validateProjection(projection: ProjectionMapping, schema: Disco
105
172
  * projection validation fail closed: we cannot prove a join is many-to-one, so
106
173
  * we refuse to compile it rather than guess.
107
174
  */
108
- declare function columnMetaFor(table: DiscoveredSchemaTable, column: string): DiscoveredColumnMeta | undefined;
175
+ export declare function columnMetaFor(table: DiscoveredSchemaTable, column: string): DiscoveredColumnMeta | undefined;
109
176
  /**
110
177
  * Every primary-key column. More than one entry means a composite PK, which is
111
178
  * why `base_pk` identity is rejected for such tables.
112
179
  */
113
- declare function primaryKeyColumns(table: DiscoveredSchemaTable): string[];
180
+ export declare function primaryKeyColumns(table: DiscoveredSchemaTable): string[];
114
181
  /**
115
182
  * Do `columns` contain every column of at least one unique key on `table`?
116
183
  *
@@ -118,7 +185,7 @@ declare function primaryKeyColumns(table: DiscoveredSchemaTable): string[];
118
185
  * joined table matches at most one row per base row; one that does not can
119
186
  * match many.
120
187
  */
121
- declare function coversUniqueKey(table: DiscoveredSchemaTable, columns: string[]): boolean;
188
+ export declare function coversUniqueKey(table: DiscoveredSchemaTable, columns: string[]): boolean;
122
189
  /**
123
190
  * A discovered table whose enrichment metadata is known to be present.
124
191
  *
@@ -142,7 +209,7 @@ type RichMetaTable = DiscoveredSchemaTable & {
142
209
  * The invariant is unchanged: this reports whether the metadata is *known*,
143
210
  * never whether the table has keys. Absent metadata stays UNKNOWN.
144
211
  */
145
- declare function hasRichMeta(table: DiscoveredSchemaTable): table is RichMetaTable;
212
+ export declare function hasRichMeta(table: DiscoveredSchemaTable): table is RichMetaTable;
146
213
  //#endregion
147
214
  //#region src/slug.d.ts
148
215
  /**
@@ -171,7 +238,7 @@ declare function hasRichMeta(table: DiscoveredSchemaTable): table is RichMetaTab
171
238
  * `"2024-invoices"` for `"2024 Invoices"`. Callers that need an
172
239
  * acceptance-guaranteed identifier want `toObjectTypeSlug`.
173
240
  */
174
- declare function slugify(value: string): string;
241
+ export declare function slugify(value: string): string;
175
242
  /**
176
243
  * A slug the server will accept, derived from a display name.
177
244
  *
@@ -193,7 +260,7 @@ declare function slugify(value: string): string;
193
260
  * decides whether that is "keep typing" or an error, since only it knows
194
261
  * whether the field is still being edited.
195
262
  */
196
- declare function toObjectTypeSlug(displayName: string): string;
263
+ export declare function toObjectTypeSlug(displayName: string): string;
197
264
  //#endregion
198
265
  //#region src/stage-rules.d.ts
199
266
  type ConditionOp = "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "before" | "after";
@@ -214,7 +281,7 @@ interface EvaluableRule {
214
281
  * rule referencing a dropped column must fail safe, because treating the field
215
282
  * as null would make `is_empty` match every deal.
216
283
  */
217
- declare function matchesCondition(condition: Condition, fields: Record<string, string | null>, mappedFields: Set<string>): boolean;
284
+ export declare function matchesCondition(condition: Condition, fields: Record<string, string | null>, mappedFields: Set<string>): boolean;
218
285
  /**
219
286
  * The id of the first rule whose conditions all hold, or null when none do.
220
287
  *
@@ -229,6 +296,6 @@ declare function matchesCondition(condition: Condition, fields: Record<string, s
229
296
  * vacuously AND to true and capture every deal, which is the opposite of what
230
297
  * a half-written rule should do.
231
298
  */
232
- declare function evaluateStage(rules: EvaluableRule[], fields: Record<string, string | null>, mappedFields: Set<string>): string | null;
299
+ export declare function evaluateStage(rules: EvaluableRule[], fields: Record<string, string | null>, mappedFields: Set<string>): string | null;
233
300
  //#endregion
234
- export { type Activity, type ActivityType, BASE_REF, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, type Company, type CompileOptions, type CompiledView, type Condition, type ConditionOp, type ConfigValidationError, type ConfigValidationResult, type ConnectionLiveness, type Contact, type ContactFilters, type ContactStatus, type CreateActivityInput, type CreateCompanyInput, type CreateContactInput, type CreateDealInput, type CreateTaskInput, type CrmConcept, type CrmMembership, type CrmRole, DEFAULT_VIEW_PREFIX, type Deal, type DealStage, type DiscoveredColumnMeta, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type EvaluableRule, type ExplainResult, type FilterExpr, type FilterValue, type IntegrationFacts, type IntegrationState, type IntegrationStatus, type IntegrationStep, MATERIALIZING_CONCEPTS, type MaterializingConcept, type MembershipResolver, type MirrorRecord, type NextAction, type Note, OPEN_STAGE_TYPE, PREVIEW_MAX_ROWS, PROJECTION_KEY_PATTERN, type PersistAction, type PipelineConfig, type PipelineKind, type PipelineStageConfig, type ProjectionFieldBinding, type ProjectionIdentity, type ProjectionJoin, type ProjectionMapping, type ProjectionRef, REQUIRED_FIELDS, type RecordSource, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type StageType, type StepId, type StepProblem, type StepState, type SyncAction, type SyncResult, type SyncStore, type Task, type TaskEntityType, type TaskFilters, type TaskPriority, type TaskStatus, type UpdateContactInput, type UpdateDealInput, type UpdateTaskInput, type ValueInterval, authCustom, buildFilterFromDiscriminator, buildRelationships, columnMetaFor, compileEntityView, compileEntityViews, compileProjectionFrom, compileProjectionIdentity, compileProjectionJoins, compileProjectionPreview, compileProjectionSelect, coversUniqueKey, defineClivlyConfig, entitiesConfigSchema, evaluateStage, explainEntitiesConfig, fanOutJoins, filterSchema, hasRichMeta, identityRefs, isClosedStageType, joinFansOut, mappingsToEntitiesConfig, matchesCondition, missingRequiredFields, primaryKeyColumns, projectionFields, projectionSchema, quoteIdent, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, requiredFieldsFor, runSync, slugify, toObjectTypeSlug, validateEntitiesConfig, validateProjection, validateProjectionFilter };
301
+ export { type Activity, type ActivityType, BASE_REF, CANONICAL_FIELDS, type CRMAdapter, CRM_CONCEPTS, type ClivlyAuthAdapter, ClivlyConfigError, type ClivlyEntitiesConfig, type ClivlyEntityConfig, type ClivlyUser, type Company, type CompanyFilters, type CompileOptions, type CompiledView, type Condition, type ConditionOp, type ConfigValidationError, type ConfigValidationResult, type ConnectionLiveness, type Contact, type ContactFilters, type ContactStatus, type CreateActivityInput, type CreateCompanyInput, type CreateContactInput, type CreateDealInput, type CreateTaskInput, type CrmConcept, type CrmMembership, type CrmRole, DEFAULT_VIEW_PREFIX, type Deal, type DealStage, type DiscoveredColumnMeta, type DiscoveredSchemaTable, type EntityMappingRow, type EntitySyncResult, type EvaluableRule, type ExplainResult, type FilterExpr, type FilterValue, type HostOwnedCompanyField, type HostOwnedContactField, type IntegrationFacts, type IntegrationState, type IntegrationStatus, type IntegrationStep, MATERIALIZING_CONCEPTS, type MaterializingConcept, type MembershipResolver, type MirrorRecord, type NextAction, type Note, OPEN_STAGE_TYPE, PROJECTION_KEY_PATTERN, type PersistAction, type PipelineConfig, type PipelineKind, type PipelineStageConfig, type ProjectionFieldBinding, type ProjectionIdentity, type ProjectionJoin, type ProjectionMapping, type ProjectionRef, REQUIRED_FIELDS, type RecordSource, type RelationshipCandidate, type RelationshipSpec, type RelationshipVia, type RelationshipsMap, type SourceRow, type SourcedRecord, type StageType, type StepId, type StepProblem, type StepState, type SyncAction, type SyncResult, type SyncStore, type Task, type TaskEntityType, type TaskFilters, type TaskPriority, type TaskStatus, type UpdateCompanyInput, type UpdateContactInput, type UpdateDealInput, type UpdateTaskInput, type ValueInterval, authCustom, buildFilterFromDiscriminator, buildRelationships, compileEntityView, compileEntityViews, defineClivlyConfig, entitiesConfigSchema, explainEntitiesConfig, filterSchema, identityRefs, isClosedStageType, mappingsToEntitiesConfig, missingRequiredFields, projectionFields, projectionSchema, reconcile, relationshipSchema, relationshipViaSignature, relationshipsSchema, requiredFieldsFor, runSync, validateEntitiesConfig };