@clivly/core 0.8.0 → 0.9.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.
- package/README.md +96 -15
- package/dist/drizzle.cjs +82 -1
- package/dist/drizzle.d.cts +23 -2
- package/dist/drizzle.d.mts +23 -2
- package/dist/drizzle.mjs +81 -2
- package/dist/entity-config-PoOGzvpN.cjs +728 -0
- package/dist/entity-config-Tb5bbKLj.mjs +621 -0
- package/dist/entity-config.cjs +12 -349
- package/dist/entity-config.d.cts +120 -1
- package/dist/entity-config.d.mts +120 -1
- package/dist/entity-config.mjs +1 -338
- package/dist/entity-heuristics.cjs +28 -8
- package/dist/entity-heuristics.d.cts +2 -12
- package/dist/entity-heuristics.d.mts +2 -12
- package/dist/entity-heuristics.mjs +28 -8
- package/dist/env-file.cjs +59 -0
- package/dist/env-file.d.cts +23 -0
- package/dist/env-file.d.mts +23 -0
- package/dist/env-file.mjs +55 -0
- package/dist/index.cjs +83 -2
- package/dist/index.d.cts +73 -3
- package/dist/index.d.mts +73 -3
- package/dist/index.mjs +69 -3
- package/dist/integration-state.cjs +242 -0
- package/dist/integration-state.d.cts +88 -0
- package/dist/integration-state.d.mts +88 -0
- package/dist/integration-state.mjs +232 -0
- package/dist/mappings-config-B0W5bVZj.d.mts +168 -0
- package/dist/mappings-config-BWo4cRDF.d.cts +168 -0
- package/dist/mappings-config.cjs +32 -6
- package/dist/mappings-config.d.cts +1 -40
- package/dist/mappings-config.d.mts +1 -40
- package/dist/mappings-config.mjs +32 -6
- package/dist/projection-BXBkh7qp.cjs +98 -0
- package/dist/projection-C_EMRmZ8.mjs +75 -0
- package/dist/sync-engine.cjs +9 -2
- package/dist/sync-engine.mjs +9 -2
- package/dist/view-compiler-BKrBA_II.mjs +254 -0
- package/dist/view-compiler-BwZ9UgLp.cjs +277 -0
- package/dist/view-compiler.cjs +5 -188
- package/dist/view-compiler.mjs +1 -184
- package/package.json +21 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { ClivlyEntitiesConfig } from "./entity-config.cjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
//#region src/projection.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Projection-backed custom mappings — one custom object derived from a base
|
|
6
|
+
* table plus explicit joins.
|
|
7
|
+
*
|
|
8
|
+
* A projection is deliberately narrow: equality joins only, direct column
|
|
9
|
+
* bindings only, no expressions and no aggregation. Everything a user can
|
|
10
|
+
* express here compiles to a single read-only view whose `id` column is
|
|
11
|
+
* provably unique per row, which is what lets the sync engine consume it
|
|
12
|
+
* unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Refs address tables symbolically: `"base"` for the base table, or a join's
|
|
15
|
+
* `key`. Raw table names are never accepted in a ref, so a projection can
|
|
16
|
+
* never reach a table it did not declare.
|
|
17
|
+
*/
|
|
18
|
+
/** The reserved ref name for the projection's base table. */
|
|
19
|
+
declare const BASE_REF = "base";
|
|
20
|
+
declare const projectionRefSchema: z.ZodObject<{
|
|
21
|
+
table: z.ZodString;
|
|
22
|
+
column: z.ZodString;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
type ProjectionRef = z.infer<typeof projectionRefSchema>;
|
|
25
|
+
declare const projectionJoinSchema: z.ZodObject<{
|
|
26
|
+
key: z.ZodString;
|
|
27
|
+
table: z.ZodString;
|
|
28
|
+
type: z.ZodEnum<{
|
|
29
|
+
inner: "inner";
|
|
30
|
+
left: "left";
|
|
31
|
+
}>;
|
|
32
|
+
on: z.ZodArray<z.ZodObject<{
|
|
33
|
+
left: z.ZodObject<{
|
|
34
|
+
table: z.ZodString;
|
|
35
|
+
column: z.ZodString;
|
|
36
|
+
}, z.core.$strict>;
|
|
37
|
+
op: z.ZodLiteral<"eq">;
|
|
38
|
+
right: z.ZodObject<{
|
|
39
|
+
table: z.ZodString;
|
|
40
|
+
column: z.ZodString;
|
|
41
|
+
}, z.core.$strict>;
|
|
42
|
+
}, z.core.$strict>>;
|
|
43
|
+
}, z.core.$strict>;
|
|
44
|
+
type ProjectionJoin = z.infer<typeof projectionJoinSchema>;
|
|
45
|
+
declare const projectionFieldBindingSchema: z.ZodObject<{
|
|
46
|
+
from: z.ZodObject<{
|
|
47
|
+
table: z.ZodString;
|
|
48
|
+
column: z.ZodString;
|
|
49
|
+
}, z.core.$strict>;
|
|
50
|
+
}, z.core.$strict>;
|
|
51
|
+
type ProjectionFieldBinding = z.infer<typeof projectionFieldBindingSchema>;
|
|
52
|
+
declare const projectionIdentitySchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
53
|
+
kind: z.ZodLiteral<"base_pk">;
|
|
54
|
+
column: z.ZodOptional<z.ZodString>;
|
|
55
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
56
|
+
kind: z.ZodLiteral<"column">;
|
|
57
|
+
from: z.ZodObject<{
|
|
58
|
+
table: z.ZodString;
|
|
59
|
+
column: z.ZodString;
|
|
60
|
+
}, z.core.$strict>;
|
|
61
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
62
|
+
kind: z.ZodLiteral<"composite">;
|
|
63
|
+
parts: z.ZodArray<z.ZodObject<{
|
|
64
|
+
table: z.ZodString;
|
|
65
|
+
column: z.ZodString;
|
|
66
|
+
}, z.core.$strict>>;
|
|
67
|
+
}, z.core.$strict>]>;
|
|
68
|
+
type ProjectionIdentity = z.infer<typeof projectionIdentitySchema>;
|
|
69
|
+
declare const projectionSchema: z.ZodObject<{
|
|
70
|
+
version: z.ZodLiteral<1>;
|
|
71
|
+
kind: z.ZodLiteral<"projection">;
|
|
72
|
+
entityKey: z.ZodString;
|
|
73
|
+
baseTable: z.ZodString;
|
|
74
|
+
joins: z.ZodArray<z.ZodObject<{
|
|
75
|
+
key: z.ZodString;
|
|
76
|
+
table: z.ZodString;
|
|
77
|
+
type: z.ZodEnum<{
|
|
78
|
+
inner: "inner";
|
|
79
|
+
left: "left";
|
|
80
|
+
}>;
|
|
81
|
+
on: z.ZodArray<z.ZodObject<{
|
|
82
|
+
left: z.ZodObject<{
|
|
83
|
+
table: z.ZodString;
|
|
84
|
+
column: z.ZodString;
|
|
85
|
+
}, z.core.$strict>;
|
|
86
|
+
op: z.ZodLiteral<"eq">;
|
|
87
|
+
right: z.ZodObject<{
|
|
88
|
+
table: z.ZodString;
|
|
89
|
+
column: z.ZodString;
|
|
90
|
+
}, z.core.$strict>;
|
|
91
|
+
}, z.core.$strict>>;
|
|
92
|
+
}, z.core.$strict>>;
|
|
93
|
+
fieldBindings: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
94
|
+
from: z.ZodObject<{
|
|
95
|
+
table: z.ZodString;
|
|
96
|
+
column: z.ZodString;
|
|
97
|
+
}, z.core.$strict>;
|
|
98
|
+
}, z.core.$strict>>;
|
|
99
|
+
identity: z.ZodUnion<readonly [z.ZodObject<{
|
|
100
|
+
kind: z.ZodLiteral<"base_pk">;
|
|
101
|
+
column: z.ZodOptional<z.ZodString>;
|
|
102
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
103
|
+
kind: z.ZodLiteral<"column">;
|
|
104
|
+
from: z.ZodObject<{
|
|
105
|
+
table: z.ZodString;
|
|
106
|
+
column: z.ZodString;
|
|
107
|
+
}, z.core.$strict>;
|
|
108
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
109
|
+
kind: z.ZodLiteral<"composite">;
|
|
110
|
+
parts: z.ZodArray<z.ZodObject<{
|
|
111
|
+
table: z.ZodString;
|
|
112
|
+
column: z.ZodString;
|
|
113
|
+
}, z.core.$strict>>;
|
|
114
|
+
}, z.core.$strict>]>;
|
|
115
|
+
}, z.core.$strict>;
|
|
116
|
+
type ProjectionMapping = z.infer<typeof projectionSchema>;
|
|
117
|
+
/**
|
|
118
|
+
* Every column ref an identity depends on. `basePkColumn` is the base table's
|
|
119
|
+
* discovered PK, used only when a `base_pk` identity omits an explicit column.
|
|
120
|
+
*/
|
|
121
|
+
declare function identityRefs(identity: ProjectionIdentity, basePkColumn: string): ProjectionRef[];
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/mappings-config.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Mappings → Entity Config bridge.
|
|
126
|
+
*
|
|
127
|
+
* `crm_entity_mappings` rows are the single source of truth for how a host
|
|
128
|
+
* schema maps onto Clivly's CRM concepts. This module compiles those rows into
|
|
129
|
+
* a `ClivlyEntitiesConfig` — the contract the view compiler and sync engine
|
|
130
|
+
* consume — so the persisted mapping and the runtime pipeline never diverge.
|
|
131
|
+
*
|
|
132
|
+
* The row model has no explicit entity-key column (it's keyed by
|
|
133
|
+
* `sourceTable + targetConcept + role`), but the config's `relationships`
|
|
134
|
+
* reference other entities *by key*. This builder therefore derives a stable
|
|
135
|
+
* key per row and resolves each relationship's `entity` reference through the
|
|
136
|
+
* same derivation — so a relationship stored as `entity: "<relatedTable>"`
|
|
137
|
+
* (the shape `mappings.suggest` produces) resolves to the right entity.
|
|
138
|
+
*/
|
|
139
|
+
/**
|
|
140
|
+
* A persisted `crm_entity_mappings` row, narrowed to the columns the builder
|
|
141
|
+
* reads. Structurally compatible with the Drizzle row (jsonb columns typed
|
|
142
|
+
* loosely) so the API layer can pass `db.select()` results straight in.
|
|
143
|
+
*/
|
|
144
|
+
interface EntityMappingRow {
|
|
145
|
+
fieldMap: Record<string, string>;
|
|
146
|
+
filter?: Record<string, unknown> | null;
|
|
147
|
+
/**
|
|
148
|
+
* Advanced mapping: a base table plus explicit joins. When set, this — not
|
|
149
|
+
* `fieldMap` — is the authored source of truth, and `sourceTable` merely
|
|
150
|
+
* mirrors `projection.baseTable`.
|
|
151
|
+
*/
|
|
152
|
+
projection?: ProjectionMapping | null;
|
|
153
|
+
relationships?: Record<string, unknown> | null;
|
|
154
|
+
role?: string | null;
|
|
155
|
+
sourceTable: string;
|
|
156
|
+
status?: string | null;
|
|
157
|
+
targetConcept: string;
|
|
158
|
+
targetObjectTypeId?: string | null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Compile persisted mapping rows into a `ClivlyEntitiesConfig`. Only `active`
|
|
162
|
+
* rows with at least one mapped field are included; disabled/suggested rows and
|
|
163
|
+
* unmapped rows are skipped. Returns `{ entities: {} }` when nothing is
|
|
164
|
+
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
165
|
+
*/
|
|
166
|
+
declare function mappingsToEntitiesConfig(rows: EntityMappingRow[]): ClivlyEntitiesConfig;
|
|
167
|
+
//#endregion
|
|
168
|
+
export { ProjectionIdentity as a, ProjectionRef as c, ProjectionFieldBinding as i, identityRefs as l, mappingsToEntitiesConfig as n, ProjectionJoin as o, BASE_REF as r, ProjectionMapping as s, EntityMappingRow as t, projectionSchema as u };
|
package/dist/mappings-config.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_entity_config = require("./entity-config.cjs");
|
|
2
|
+
const require_entity_config = require("./entity-config-PoOGzvpN.cjs");
|
|
3
3
|
//#region src/mappings-config.ts
|
|
4
4
|
const RESERVED_FIELD = "id";
|
|
5
5
|
/**
|
|
@@ -22,7 +22,9 @@ function canonicalFields(concept, fieldMap) {
|
|
|
22
22
|
const DEFAULT_ROLE = "primary";
|
|
23
23
|
const ACTIVE_STATUS = "active";
|
|
24
24
|
function isEligible(row) {
|
|
25
|
-
|
|
25
|
+
if ((row.status ?? ACTIVE_STATUS) !== ACTIVE_STATUS) return false;
|
|
26
|
+
if (row.projection) return Object.keys(row.projection.fieldBindings ?? {}).length > 0;
|
|
27
|
+
return Object.keys(row.fieldMap ?? {}).length > 0;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Preferred key for a row: the bare source table when it backs a single entity,
|
|
@@ -46,13 +48,28 @@ function uniquify(base, used) {
|
|
|
46
48
|
used.add(key);
|
|
47
49
|
return key;
|
|
48
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* A projection's `fields` map, derived mechanically from its bindings.
|
|
53
|
+
*
|
|
54
|
+
* `ClivlyEntityConfig.fields` is required and read by existing consumers, so a
|
|
55
|
+
* projection entity must populate it. The values are ref *tokens*
|
|
56
|
+
* (`"base.full_name"`), never host column names — the compiler reads
|
|
57
|
+
* `projection.fieldBindings`, and `validateEntitiesConfig` skips the legacy
|
|
58
|
+
* column check for projection entities precisely because of this.
|
|
59
|
+
*/
|
|
60
|
+
function projectionFields(projection) {
|
|
61
|
+
const fields = {};
|
|
62
|
+
for (const [field, binding] of Object.entries(projection.fieldBindings)) fields[field] = `${binding.from.table}.${binding.from.column}`;
|
|
63
|
+
return fields;
|
|
64
|
+
}
|
|
49
65
|
function buildEntity(row, refIndex) {
|
|
50
66
|
const entity = {
|
|
51
67
|
concept: row.targetConcept,
|
|
52
|
-
source: row.sourceTable,
|
|
68
|
+
source: row.projection?.baseTable ?? row.sourceTable,
|
|
53
69
|
role: row.role ?? DEFAULT_ROLE,
|
|
54
|
-
fields: canonicalFields(row.targetConcept, row.fieldMap)
|
|
70
|
+
fields: row.projection ? projectionFields(row.projection) : canonicalFields(row.targetConcept, row.fieldMap)
|
|
55
71
|
};
|
|
72
|
+
if (row.projection) entity.projection = row.projection;
|
|
56
73
|
if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
|
|
57
74
|
if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
|
|
58
75
|
if (row.relationships && Object.keys(row.relationships).length > 0) {
|
|
@@ -77,11 +94,16 @@ function buildEntity(row, refIndex) {
|
|
|
77
94
|
function mappingsToEntitiesConfig(rows) {
|
|
78
95
|
const eligible = rows.filter(isEligible);
|
|
79
96
|
const tableCounts = /* @__PURE__ */ new Map();
|
|
80
|
-
for (const row of eligible)
|
|
97
|
+
for (const row of eligible) {
|
|
98
|
+
if (row.projection) continue;
|
|
99
|
+
tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
|
|
100
|
+
}
|
|
81
101
|
const used = /* @__PURE__ */ new Set();
|
|
82
102
|
const tableAnchor = /* @__PURE__ */ new Map();
|
|
83
103
|
const assigned = [];
|
|
84
|
-
|
|
104
|
+
const legacyRows = eligible.filter((row) => !row.projection);
|
|
105
|
+
const projectionRows = eligible.filter((row) => row.projection);
|
|
106
|
+
for (const row of legacyRows) {
|
|
85
107
|
const key = uniquify(preferredKey(row, tableCounts), used);
|
|
86
108
|
assigned.push({
|
|
87
109
|
row,
|
|
@@ -89,6 +111,10 @@ function mappingsToEntitiesConfig(rows) {
|
|
|
89
111
|
});
|
|
90
112
|
if ((row.role ?? DEFAULT_ROLE) === DEFAULT_ROLE || !tableAnchor.has(row.sourceTable)) tableAnchor.set(row.sourceTable, key);
|
|
91
113
|
}
|
|
114
|
+
for (const row of projectionRows) assigned.push({
|
|
115
|
+
row,
|
|
116
|
+
key: uniquify(row.projection?.entityKey ?? row.sourceTable, used)
|
|
117
|
+
});
|
|
92
118
|
const refIndex = /* @__PURE__ */ new Map();
|
|
93
119
|
for (const { key } of assigned) refIndex.set(key, key);
|
|
94
120
|
for (const [table, key] of tableAnchor) if (!refIndex.has(table)) refIndex.set(table, key);
|
|
@@ -1,41 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/mappings-config.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Mappings → Entity Config bridge.
|
|
5
|
-
*
|
|
6
|
-
* `crm_entity_mappings` rows are the single source of truth for how a host
|
|
7
|
-
* schema maps onto Clivly's CRM concepts. This module compiles those rows into
|
|
8
|
-
* a `ClivlyEntitiesConfig` — the contract the view compiler and sync engine
|
|
9
|
-
* consume — so the persisted mapping and the runtime pipeline never diverge.
|
|
10
|
-
*
|
|
11
|
-
* The row model has no explicit entity-key column (it's keyed by
|
|
12
|
-
* `sourceTable + targetConcept + role`), but the config's `relationships`
|
|
13
|
-
* reference other entities *by key*. This builder therefore derives a stable
|
|
14
|
-
* key per row and resolves each relationship's `entity` reference through the
|
|
15
|
-
* same derivation — so a relationship stored as `entity: "<relatedTable>"`
|
|
16
|
-
* (the shape `mappings.suggest` produces) resolves to the right entity.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* A persisted `crm_entity_mappings` row, narrowed to the columns the builder
|
|
20
|
-
* reads. Structurally compatible with the Drizzle row (jsonb columns typed
|
|
21
|
-
* loosely) so the API layer can pass `db.select()` results straight in.
|
|
22
|
-
*/
|
|
23
|
-
interface EntityMappingRow {
|
|
24
|
-
fieldMap: Record<string, string>;
|
|
25
|
-
filter?: Record<string, unknown> | null;
|
|
26
|
-
relationships?: Record<string, unknown> | null;
|
|
27
|
-
role?: string | null;
|
|
28
|
-
sourceTable: string;
|
|
29
|
-
status?: string | null;
|
|
30
|
-
targetConcept: string;
|
|
31
|
-
targetObjectTypeId?: string | null;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Compile persisted mapping rows into a `ClivlyEntitiesConfig`. Only `active`
|
|
35
|
-
* rows with at least one mapped field are included; disabled/suggested rows and
|
|
36
|
-
* unmapped rows are skipped. Returns `{ entities: {} }` when nothing is
|
|
37
|
-
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
38
|
-
*/
|
|
39
|
-
declare function mappingsToEntitiesConfig(rows: EntityMappingRow[]): ClivlyEntitiesConfig;
|
|
40
|
-
//#endregion
|
|
1
|
+
import { n as mappingsToEntitiesConfig, t as EntityMappingRow } from "./mappings-config-BWo4cRDF.cjs";
|
|
41
2
|
export { EntityMappingRow, mappingsToEntitiesConfig };
|
|
@@ -1,41 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
//#region src/mappings-config.d.ts
|
|
3
|
-
/**
|
|
4
|
-
* Mappings → Entity Config bridge.
|
|
5
|
-
*
|
|
6
|
-
* `crm_entity_mappings` rows are the single source of truth for how a host
|
|
7
|
-
* schema maps onto Clivly's CRM concepts. This module compiles those rows into
|
|
8
|
-
* a `ClivlyEntitiesConfig` — the contract the view compiler and sync engine
|
|
9
|
-
* consume — so the persisted mapping and the runtime pipeline never diverge.
|
|
10
|
-
*
|
|
11
|
-
* The row model has no explicit entity-key column (it's keyed by
|
|
12
|
-
* `sourceTable + targetConcept + role`), but the config's `relationships`
|
|
13
|
-
* reference other entities *by key*. This builder therefore derives a stable
|
|
14
|
-
* key per row and resolves each relationship's `entity` reference through the
|
|
15
|
-
* same derivation — so a relationship stored as `entity: "<relatedTable>"`
|
|
16
|
-
* (the shape `mappings.suggest` produces) resolves to the right entity.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* A persisted `crm_entity_mappings` row, narrowed to the columns the builder
|
|
20
|
-
* reads. Structurally compatible with the Drizzle row (jsonb columns typed
|
|
21
|
-
* loosely) so the API layer can pass `db.select()` results straight in.
|
|
22
|
-
*/
|
|
23
|
-
interface EntityMappingRow {
|
|
24
|
-
fieldMap: Record<string, string>;
|
|
25
|
-
filter?: Record<string, unknown> | null;
|
|
26
|
-
relationships?: Record<string, unknown> | null;
|
|
27
|
-
role?: string | null;
|
|
28
|
-
sourceTable: string;
|
|
29
|
-
status?: string | null;
|
|
30
|
-
targetConcept: string;
|
|
31
|
-
targetObjectTypeId?: string | null;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Compile persisted mapping rows into a `ClivlyEntitiesConfig`. Only `active`
|
|
35
|
-
* rows with at least one mapped field are included; disabled/suggested rows and
|
|
36
|
-
* unmapped rows are skipped. Returns `{ entities: {} }` when nothing is
|
|
37
|
-
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
38
|
-
*/
|
|
39
|
-
declare function mappingsToEntitiesConfig(rows: EntityMappingRow[]): ClivlyEntitiesConfig;
|
|
40
|
-
//#endregion
|
|
1
|
+
import { n as mappingsToEntitiesConfig, t as EntityMappingRow } from "./mappings-config-B0W5bVZj.mjs";
|
|
41
2
|
export { EntityMappingRow, mappingsToEntitiesConfig };
|
package/dist/mappings-config.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CANONICAL_FIELDS } from "./entity-config.mjs";
|
|
1
|
+
import { t as CANONICAL_FIELDS } from "./entity-config-Tb5bbKLj.mjs";
|
|
2
2
|
//#region src/mappings-config.ts
|
|
3
3
|
const RESERVED_FIELD = "id";
|
|
4
4
|
/**
|
|
@@ -21,7 +21,9 @@ function canonicalFields(concept, fieldMap) {
|
|
|
21
21
|
const DEFAULT_ROLE = "primary";
|
|
22
22
|
const ACTIVE_STATUS = "active";
|
|
23
23
|
function isEligible(row) {
|
|
24
|
-
|
|
24
|
+
if ((row.status ?? ACTIVE_STATUS) !== ACTIVE_STATUS) return false;
|
|
25
|
+
if (row.projection) return Object.keys(row.projection.fieldBindings ?? {}).length > 0;
|
|
26
|
+
return Object.keys(row.fieldMap ?? {}).length > 0;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* Preferred key for a row: the bare source table when it backs a single entity,
|
|
@@ -45,13 +47,28 @@ function uniquify(base, used) {
|
|
|
45
47
|
used.add(key);
|
|
46
48
|
return key;
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* A projection's `fields` map, derived mechanically from its bindings.
|
|
52
|
+
*
|
|
53
|
+
* `ClivlyEntityConfig.fields` is required and read by existing consumers, so a
|
|
54
|
+
* projection entity must populate it. The values are ref *tokens*
|
|
55
|
+
* (`"base.full_name"`), never host column names — the compiler reads
|
|
56
|
+
* `projection.fieldBindings`, and `validateEntitiesConfig` skips the legacy
|
|
57
|
+
* column check for projection entities precisely because of this.
|
|
58
|
+
*/
|
|
59
|
+
function projectionFields(projection) {
|
|
60
|
+
const fields = {};
|
|
61
|
+
for (const [field, binding] of Object.entries(projection.fieldBindings)) fields[field] = `${binding.from.table}.${binding.from.column}`;
|
|
62
|
+
return fields;
|
|
63
|
+
}
|
|
48
64
|
function buildEntity(row, refIndex) {
|
|
49
65
|
const entity = {
|
|
50
66
|
concept: row.targetConcept,
|
|
51
|
-
source: row.sourceTable,
|
|
67
|
+
source: row.projection?.baseTable ?? row.sourceTable,
|
|
52
68
|
role: row.role ?? DEFAULT_ROLE,
|
|
53
|
-
fields: canonicalFields(row.targetConcept, row.fieldMap)
|
|
69
|
+
fields: row.projection ? projectionFields(row.projection) : canonicalFields(row.targetConcept, row.fieldMap)
|
|
54
70
|
};
|
|
71
|
+
if (row.projection) entity.projection = row.projection;
|
|
55
72
|
if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
|
|
56
73
|
if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
|
|
57
74
|
if (row.relationships && Object.keys(row.relationships).length > 0) {
|
|
@@ -76,11 +93,16 @@ function buildEntity(row, refIndex) {
|
|
|
76
93
|
function mappingsToEntitiesConfig(rows) {
|
|
77
94
|
const eligible = rows.filter(isEligible);
|
|
78
95
|
const tableCounts = /* @__PURE__ */ new Map();
|
|
79
|
-
for (const row of eligible)
|
|
96
|
+
for (const row of eligible) {
|
|
97
|
+
if (row.projection) continue;
|
|
98
|
+
tableCounts.set(row.sourceTable, (tableCounts.get(row.sourceTable) ?? 0) + 1);
|
|
99
|
+
}
|
|
80
100
|
const used = /* @__PURE__ */ new Set();
|
|
81
101
|
const tableAnchor = /* @__PURE__ */ new Map();
|
|
82
102
|
const assigned = [];
|
|
83
|
-
|
|
103
|
+
const legacyRows = eligible.filter((row) => !row.projection);
|
|
104
|
+
const projectionRows = eligible.filter((row) => row.projection);
|
|
105
|
+
for (const row of legacyRows) {
|
|
84
106
|
const key = uniquify(preferredKey(row, tableCounts), used);
|
|
85
107
|
assigned.push({
|
|
86
108
|
row,
|
|
@@ -88,6 +110,10 @@ function mappingsToEntitiesConfig(rows) {
|
|
|
88
110
|
});
|
|
89
111
|
if ((row.role ?? DEFAULT_ROLE) === DEFAULT_ROLE || !tableAnchor.has(row.sourceTable)) tableAnchor.set(row.sourceTable, key);
|
|
90
112
|
}
|
|
113
|
+
for (const row of projectionRows) assigned.push({
|
|
114
|
+
row,
|
|
115
|
+
key: uniquify(row.projection?.entityKey ?? row.sourceTable, used)
|
|
116
|
+
});
|
|
91
117
|
const refIndex = /* @__PURE__ */ new Map();
|
|
92
118
|
for (const { key } of assigned) refIndex.set(key, key);
|
|
93
119
|
for (const [table, key] of tableAnchor) if (!refIndex.has(table)) refIndex.set(table, key);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
let zod = require("zod");
|
|
2
|
+
//#region src/projection.ts
|
|
3
|
+
/**
|
|
4
|
+
* Projection-backed custom mappings — one custom object derived from a base
|
|
5
|
+
* table plus explicit joins.
|
|
6
|
+
*
|
|
7
|
+
* A projection is deliberately narrow: equality joins only, direct column
|
|
8
|
+
* bindings only, no expressions and no aggregation. Everything a user can
|
|
9
|
+
* express here compiles to a single read-only view whose `id` column is
|
|
10
|
+
* provably unique per row, which is what lets the sync engine consume it
|
|
11
|
+
* unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Refs address tables symbolically: `"base"` for the base table, or a join's
|
|
14
|
+
* `key`. Raw table names are never accepted in a ref, so a projection can
|
|
15
|
+
* never reach a table it did not declare.
|
|
16
|
+
*/
|
|
17
|
+
/** The reserved ref name for the projection's base table. */
|
|
18
|
+
const BASE_REF = "base";
|
|
19
|
+
/** Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs. */
|
|
20
|
+
const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
21
|
+
const projectionRefSchema = zod.z.strictObject({
|
|
22
|
+
table: zod.z.string().min(1),
|
|
23
|
+
column: zod.z.string().min(1)
|
|
24
|
+
});
|
|
25
|
+
const projectionJoinConditionSchema = zod.z.strictObject({
|
|
26
|
+
left: projectionRefSchema,
|
|
27
|
+
op: zod.z.literal("eq"),
|
|
28
|
+
right: projectionRefSchema
|
|
29
|
+
});
|
|
30
|
+
const projectionJoinSchema = zod.z.strictObject({
|
|
31
|
+
/** SQL alias and ref target. Unique within the projection. */
|
|
32
|
+
key: zod.z.string().regex(PROJECTION_KEY_PATTERN),
|
|
33
|
+
table: zod.z.string().min(1),
|
|
34
|
+
type: zod.z.enum(["inner", "left"]),
|
|
35
|
+
on: zod.z.array(projectionJoinConditionSchema).min(1)
|
|
36
|
+
});
|
|
37
|
+
const projectionFieldBindingSchema = zod.z.strictObject({ from: projectionRefSchema });
|
|
38
|
+
const projectionIdentitySchema = zod.z.union([
|
|
39
|
+
zod.z.strictObject({
|
|
40
|
+
kind: zod.z.literal("base_pk"),
|
|
41
|
+
/** Defaults to the discovered single-column PK of the base table. */
|
|
42
|
+
column: zod.z.string().min(1).optional()
|
|
43
|
+
}),
|
|
44
|
+
zod.z.strictObject({
|
|
45
|
+
kind: zod.z.literal("column"),
|
|
46
|
+
from: projectionRefSchema
|
|
47
|
+
}),
|
|
48
|
+
zod.z.strictObject({
|
|
49
|
+
kind: zod.z.literal("composite"),
|
|
50
|
+
parts: zod.z.array(projectionRefSchema).min(2)
|
|
51
|
+
})
|
|
52
|
+
]);
|
|
53
|
+
const projectionSchema = zod.z.strictObject({
|
|
54
|
+
version: zod.z.literal(1),
|
|
55
|
+
kind: zod.z.literal("projection"),
|
|
56
|
+
entityKey: zod.z.string().regex(PROJECTION_KEY_PATTERN),
|
|
57
|
+
baseTable: zod.z.string().min(1),
|
|
58
|
+
joins: zod.z.array(projectionJoinSchema),
|
|
59
|
+
fieldBindings: zod.z.record(zod.z.string(), projectionFieldBindingSchema).refine((b) => Object.keys(b).length > 0, { message: "at least one field must be bound" }),
|
|
60
|
+
identity: projectionIdentitySchema
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Every column ref an identity depends on. `basePkColumn` is the base table's
|
|
64
|
+
* discovered PK, used only when a `base_pk` identity omits an explicit column.
|
|
65
|
+
*/
|
|
66
|
+
function identityRefs(identity, basePkColumn) {
|
|
67
|
+
if (identity.kind === "base_pk") return [{
|
|
68
|
+
table: BASE_REF,
|
|
69
|
+
column: identity.column ?? basePkColumn
|
|
70
|
+
}];
|
|
71
|
+
if (identity.kind === "column") return [identity.from];
|
|
72
|
+
return identity.parts;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
Object.defineProperty(exports, "BASE_REF", {
|
|
76
|
+
enumerable: true,
|
|
77
|
+
get: function() {
|
|
78
|
+
return BASE_REF;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(exports, "PROJECTION_KEY_PATTERN", {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
get: function() {
|
|
84
|
+
return PROJECTION_KEY_PATTERN;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
Object.defineProperty(exports, "identityRefs", {
|
|
88
|
+
enumerable: true,
|
|
89
|
+
get: function() {
|
|
90
|
+
return identityRefs;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
Object.defineProperty(exports, "projectionSchema", {
|
|
94
|
+
enumerable: true,
|
|
95
|
+
get: function() {
|
|
96
|
+
return projectionSchema;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/projection.ts
|
|
3
|
+
/**
|
|
4
|
+
* Projection-backed custom mappings — one custom object derived from a base
|
|
5
|
+
* table plus explicit joins.
|
|
6
|
+
*
|
|
7
|
+
* A projection is deliberately narrow: equality joins only, direct column
|
|
8
|
+
* bindings only, no expressions and no aggregation. Everything a user can
|
|
9
|
+
* express here compiles to a single read-only view whose `id` column is
|
|
10
|
+
* provably unique per row, which is what lets the sync engine consume it
|
|
11
|
+
* unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Refs address tables symbolically: `"base"` for the base table, or a join's
|
|
14
|
+
* `key`. Raw table names are never accepted in a ref, so a projection can
|
|
15
|
+
* never reach a table it did not declare.
|
|
16
|
+
*/
|
|
17
|
+
/** The reserved ref name for the projection's base table. */
|
|
18
|
+
const BASE_REF = "base";
|
|
19
|
+
/** Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs. */
|
|
20
|
+
const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
21
|
+
const projectionRefSchema = z.strictObject({
|
|
22
|
+
table: z.string().min(1),
|
|
23
|
+
column: z.string().min(1)
|
|
24
|
+
});
|
|
25
|
+
const projectionJoinConditionSchema = z.strictObject({
|
|
26
|
+
left: projectionRefSchema,
|
|
27
|
+
op: z.literal("eq"),
|
|
28
|
+
right: projectionRefSchema
|
|
29
|
+
});
|
|
30
|
+
const projectionJoinSchema = z.strictObject({
|
|
31
|
+
/** SQL alias and ref target. Unique within the projection. */
|
|
32
|
+
key: z.string().regex(PROJECTION_KEY_PATTERN),
|
|
33
|
+
table: z.string().min(1),
|
|
34
|
+
type: z.enum(["inner", "left"]),
|
|
35
|
+
on: z.array(projectionJoinConditionSchema).min(1)
|
|
36
|
+
});
|
|
37
|
+
const projectionFieldBindingSchema = z.strictObject({ from: projectionRefSchema });
|
|
38
|
+
const projectionIdentitySchema = z.union([
|
|
39
|
+
z.strictObject({
|
|
40
|
+
kind: z.literal("base_pk"),
|
|
41
|
+
/** Defaults to the discovered single-column PK of the base table. */
|
|
42
|
+
column: z.string().min(1).optional()
|
|
43
|
+
}),
|
|
44
|
+
z.strictObject({
|
|
45
|
+
kind: z.literal("column"),
|
|
46
|
+
from: projectionRefSchema
|
|
47
|
+
}),
|
|
48
|
+
z.strictObject({
|
|
49
|
+
kind: z.literal("composite"),
|
|
50
|
+
parts: z.array(projectionRefSchema).min(2)
|
|
51
|
+
})
|
|
52
|
+
]);
|
|
53
|
+
const projectionSchema = z.strictObject({
|
|
54
|
+
version: z.literal(1),
|
|
55
|
+
kind: z.literal("projection"),
|
|
56
|
+
entityKey: z.string().regex(PROJECTION_KEY_PATTERN),
|
|
57
|
+
baseTable: z.string().min(1),
|
|
58
|
+
joins: z.array(projectionJoinSchema),
|
|
59
|
+
fieldBindings: z.record(z.string(), projectionFieldBindingSchema).refine((b) => Object.keys(b).length > 0, { message: "at least one field must be bound" }),
|
|
60
|
+
identity: projectionIdentitySchema
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Every column ref an identity depends on. `basePkColumn` is the base table's
|
|
64
|
+
* discovered PK, used only when a `base_pk` identity omits an explicit column.
|
|
65
|
+
*/
|
|
66
|
+
function identityRefs(identity, basePkColumn) {
|
|
67
|
+
if (identity.kind === "base_pk") return [{
|
|
68
|
+
table: BASE_REF,
|
|
69
|
+
column: identity.column ?? basePkColumn
|
|
70
|
+
}];
|
|
71
|
+
if (identity.kind === "column") return [identity.from];
|
|
72
|
+
return identity.parts;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { projectionSchema as i, PROJECTION_KEY_PATTERN as n, identityRefs as r, BASE_REF as t };
|
package/dist/sync-engine.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_view_compiler = require("./view-compiler.cjs");
|
|
2
|
+
const require_view_compiler = require("./view-compiler-BwZ9UgLp.cjs");
|
|
3
3
|
//#region src/sync-engine.ts
|
|
4
4
|
/**
|
|
5
5
|
* Diff a view's rows against the current mirror population for one entity.
|
|
@@ -171,10 +171,17 @@ function isEmptyValue(value) {
|
|
|
171
171
|
*/
|
|
172
172
|
function computeSampleWarnings(rows, requiredFields, sourceTable) {
|
|
173
173
|
if (rows.length === 0) return [{ message: `no rows found in ${sourceTable}` }];
|
|
174
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
175
|
+
const duplicateRefs = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const row of rows) {
|
|
177
|
+
if (seenRefs.has(row.sourceRef)) duplicateRefs.add(row.sourceRef);
|
|
178
|
+
seenRefs.add(row.sourceRef);
|
|
179
|
+
}
|
|
180
|
+
const identityWarnings = duplicateRefs.size > 0 ? [{ message: `duplicate id in ${duplicateRefs.size} of ${rows.length} sampled rows — identity is not unique, so sync would collapse these into one record` }] : [];
|
|
174
181
|
const required = new Set(requiredFields);
|
|
175
182
|
const fields = new Set(requiredFields);
|
|
176
183
|
for (const row of rows) for (const key of Object.keys(row.fields)) fields.add(key);
|
|
177
|
-
const warnings = [];
|
|
184
|
+
const warnings = [...identityWarnings];
|
|
178
185
|
for (const field of [...fields].sort()) {
|
|
179
186
|
const emptyCount = rows.filter((row) => isEmptyValue(row.fields[field])).length;
|
|
180
187
|
if (required.has(field)) {
|
package/dist/sync-engine.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compileEntityViews } from "./view-compiler.mjs";
|
|
1
|
+
import { r as compileEntityViews } from "./view-compiler-BKrBA_II.mjs";
|
|
2
2
|
//#region src/sync-engine.ts
|
|
3
3
|
/**
|
|
4
4
|
* Diff a view's rows against the current mirror population for one entity.
|
|
@@ -170,10 +170,17 @@ function isEmptyValue(value) {
|
|
|
170
170
|
*/
|
|
171
171
|
function computeSampleWarnings(rows, requiredFields, sourceTable) {
|
|
172
172
|
if (rows.length === 0) return [{ message: `no rows found in ${sourceTable}` }];
|
|
173
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
174
|
+
const duplicateRefs = /* @__PURE__ */ new Set();
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
if (seenRefs.has(row.sourceRef)) duplicateRefs.add(row.sourceRef);
|
|
177
|
+
seenRefs.add(row.sourceRef);
|
|
178
|
+
}
|
|
179
|
+
const identityWarnings = duplicateRefs.size > 0 ? [{ message: `duplicate id in ${duplicateRefs.size} of ${rows.length} sampled rows — identity is not unique, so sync would collapse these into one record` }] : [];
|
|
173
180
|
const required = new Set(requiredFields);
|
|
174
181
|
const fields = new Set(requiredFields);
|
|
175
182
|
for (const row of rows) for (const key of Object.keys(row.fields)) fields.add(key);
|
|
176
|
-
const warnings = [];
|
|
183
|
+
const warnings = [...identityWarnings];
|
|
177
184
|
for (const field of [...fields].sort()) {
|
|
178
185
|
const emptyCount = rows.filter((row) => isEmptyValue(row.fields[field])).length;
|
|
179
186
|
if (required.has(field)) {
|