@clivly/core 0.2.0 → 0.3.0-next.1
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/dist/adapter.d.cts +13 -3
- package/dist/adapter.d.mts +13 -3
- package/dist/entity-config.cjs +39 -3
- package/dist/entity-config.d.cts +29 -5
- package/dist/entity-config.d.mts +29 -5
- package/dist/entity-config.mjs +38 -4
- package/dist/entity-heuristics.cjs +160 -62
- package/dist/entity-heuristics.d.cts +38 -13
- package/dist/entity-heuristics.d.mts +38 -13
- package/dist/entity-heuristics.mjs +156 -60
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/mapping-score.cjs +97 -0
- package/dist/mapping-score.d.cts +49 -0
- package/dist/mapping-score.d.mts +49 -0
- package/dist/mapping-score.mjs +87 -0
- package/dist/mappings-config.cjs +1 -0
- package/dist/mappings-config.d.cts +1 -0
- package/dist/mappings-config.d.mts +1 -0
- package/dist/mappings-config.mjs +1 -0
- package/dist/sync-engine.cjs +54 -4
- package/dist/sync-engine.d.cts +40 -5
- package/dist/sync-engine.d.mts +40 -5
- package/dist/sync-engine.mjs +53 -5
- package/dist/types.d.cts +4 -1
- package/dist/types.d.mts +4 -1
- package/dist/view-compiler.d.cts +7 -0
- package/dist/view-compiler.d.mts +7 -0
- package/package.json +12 -2
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//#region src/mapping-score.ts
|
|
2
|
+
const CONCEPT_TIERS = {
|
|
3
|
+
high: 70,
|
|
4
|
+
medium: 40
|
|
5
|
+
};
|
|
6
|
+
const FIELD_TIERS = {
|
|
7
|
+
high: 70,
|
|
8
|
+
medium: 40
|
|
9
|
+
};
|
|
10
|
+
const RELATIONSHIP_TIERS = {
|
|
11
|
+
high: 60,
|
|
12
|
+
medium: 30
|
|
13
|
+
};
|
|
14
|
+
/** Two concept candidates closer than this are treated as ambiguous. */
|
|
15
|
+
const AMBIGUITY_MARGIN = 15;
|
|
16
|
+
/** Signal weights. Tuned against fixtures; adjust here, not at call sites. */
|
|
17
|
+
const W = {
|
|
18
|
+
nameExact: 50,
|
|
19
|
+
nameSubstring: 25,
|
|
20
|
+
definingColumn: 20,
|
|
21
|
+
hasPrimaryKey: 10,
|
|
22
|
+
hasRows: 10,
|
|
23
|
+
emptyTable: -20,
|
|
24
|
+
fieldNameMatch: 50,
|
|
25
|
+
typeCompatible: 30,
|
|
26
|
+
typeMismatch: -30,
|
|
27
|
+
requiredNotNull: 10,
|
|
28
|
+
fkResolved: 60,
|
|
29
|
+
fkNameHint: 30,
|
|
30
|
+
joinTable: 20
|
|
31
|
+
};
|
|
32
|
+
function tierFor(score, t) {
|
|
33
|
+
if (score >= t.high) return "High";
|
|
34
|
+
if (score >= t.medium) return "Medium";
|
|
35
|
+
return "Low";
|
|
36
|
+
}
|
|
37
|
+
const TEXT_TYPES = [
|
|
38
|
+
"text",
|
|
39
|
+
"varchar",
|
|
40
|
+
"char",
|
|
41
|
+
"citext",
|
|
42
|
+
"uuid",
|
|
43
|
+
"string"
|
|
44
|
+
];
|
|
45
|
+
const NUMERIC_TYPES = [
|
|
46
|
+
"numeric",
|
|
47
|
+
"decimal",
|
|
48
|
+
"int",
|
|
49
|
+
"integer",
|
|
50
|
+
"bigint",
|
|
51
|
+
"smallint",
|
|
52
|
+
"float",
|
|
53
|
+
"double",
|
|
54
|
+
"real",
|
|
55
|
+
"money"
|
|
56
|
+
];
|
|
57
|
+
const TIMESTAMP_TYPES = [
|
|
58
|
+
"timestamp",
|
|
59
|
+
"timestamptz",
|
|
60
|
+
"date",
|
|
61
|
+
"datetime"
|
|
62
|
+
];
|
|
63
|
+
function matchesType(type, list) {
|
|
64
|
+
if (!type) return false;
|
|
65
|
+
const normalized = type.toLowerCase();
|
|
66
|
+
return list.some((t) => normalized.startsWith(t));
|
|
67
|
+
}
|
|
68
|
+
function isTextLike(type) {
|
|
69
|
+
return matchesType(type, TEXT_TYPES);
|
|
70
|
+
}
|
|
71
|
+
function isNumericLike(type) {
|
|
72
|
+
return matchesType(type, NUMERIC_TYPES);
|
|
73
|
+
}
|
|
74
|
+
function isTimestampLike(type) {
|
|
75
|
+
return matchesType(type, TIMESTAMP_TYPES);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* True when the top two candidates are too close to call. Callers must never
|
|
79
|
+
* one-click apply an ambiguous result — the user picks.
|
|
80
|
+
*/
|
|
81
|
+
function isAmbiguous(ranked) {
|
|
82
|
+
const [first, second] = ranked;
|
|
83
|
+
if (!(first && second)) return false;
|
|
84
|
+
return first.score - second.score < 15;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { AMBIGUITY_MARGIN, CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor };
|
package/dist/mappings-config.cjs
CHANGED
|
@@ -53,6 +53,7 @@ function buildEntity(row, refIndex) {
|
|
|
53
53
|
role: row.role ?? DEFAULT_ROLE,
|
|
54
54
|
fields: canonicalFields(row.targetConcept, row.fieldMap)
|
|
55
55
|
};
|
|
56
|
+
if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
|
|
56
57
|
if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
|
|
57
58
|
if (row.relationships && Object.keys(row.relationships).length > 0) {
|
|
58
59
|
const relationships = {};
|
package/dist/mappings-config.mjs
CHANGED
|
@@ -52,6 +52,7 @@ function buildEntity(row, refIndex) {
|
|
|
52
52
|
role: row.role ?? DEFAULT_ROLE,
|
|
53
53
|
fields: canonicalFields(row.targetConcept, row.fieldMap)
|
|
54
54
|
};
|
|
55
|
+
if (row.targetConcept === "custom" && row.targetObjectTypeId) entity.targetObjectTypeId = row.targetObjectTypeId;
|
|
55
56
|
if (row.filter && Object.keys(row.filter).length > 0) entity.filter = row.filter;
|
|
56
57
|
if (row.relationships && Object.keys(row.relationships).length > 0) {
|
|
57
58
|
const relationships = {};
|
package/dist/sync-engine.cjs
CHANGED
|
@@ -114,7 +114,8 @@ function isCompositeEntity(config, entityKey) {
|
|
|
114
114
|
* companies first. Returns per-entity and total action counts.
|
|
115
115
|
*/
|
|
116
116
|
async function runSync(config, store, options = {}) {
|
|
117
|
-
const
|
|
117
|
+
const allViews = require_view_compiler.compileEntityViews(config, options).sort(syncOrder);
|
|
118
|
+
const views = options.sample ? allViews.filter((v) => v.entityKey === options.sample?.entityKey) : allViews;
|
|
118
119
|
const entities = [];
|
|
119
120
|
const totals = {
|
|
120
121
|
created: 0,
|
|
@@ -123,15 +124,18 @@ async function runSync(config, store, options = {}) {
|
|
|
123
124
|
archived: 0
|
|
124
125
|
};
|
|
125
126
|
for (const view of views) {
|
|
126
|
-
const
|
|
127
|
+
const objectTypeId = config.entities[view.entityKey]?.targetObjectTypeId;
|
|
128
|
+
const [source, mirror] = await Promise.all([store.readView(view, options.sample ? { limit: 1 } : void 0), store.listMirror({
|
|
127
129
|
concept: view.concept,
|
|
128
|
-
role: view.role
|
|
130
|
+
role: view.role,
|
|
131
|
+
objectTypeId
|
|
129
132
|
})]);
|
|
130
|
-
const actions = reconcile(source, mirror, { composite: isCompositeEntity(config, view.entityKey) });
|
|
133
|
+
const actions = reconcile(source, mirror, { composite: isCompositeEntity(config, view.entityKey) }).filter((a) => !(options.sample && a.kind === "archive"));
|
|
131
134
|
const persistActions = await resolveActions(store, actions);
|
|
132
135
|
await store.persist({
|
|
133
136
|
concept: view.concept,
|
|
134
137
|
role: view.role,
|
|
138
|
+
objectTypeId,
|
|
135
139
|
actions: persistActions
|
|
136
140
|
});
|
|
137
141
|
const counts = emptyTally();
|
|
@@ -156,6 +160,52 @@ async function runSync(config, store, options = {}) {
|
|
|
156
160
|
totals
|
|
157
161
|
};
|
|
158
162
|
}
|
|
163
|
+
function isEmptyValue(value) {
|
|
164
|
+
return value === null || value === void 0 || value === "";
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Sample-scoped data-quality warnings over projected preview rows. Each mapped
|
|
168
|
+
* (or required-but-unmapped) field yields at most one warning, by precedence:
|
|
169
|
+
* a required field empty in ≥1 row → `K/N` form; an optional field empty in
|
|
170
|
+
* ALL rows → all-empty form; zero source rows → a single table-level warning.
|
|
171
|
+
*/
|
|
172
|
+
function computeSampleWarnings(rows, requiredFields, sourceTable) {
|
|
173
|
+
if (rows.length === 0) return [{ message: `no rows found in ${sourceTable}` }];
|
|
174
|
+
const required = new Set(requiredFields);
|
|
175
|
+
const fields = new Set(requiredFields);
|
|
176
|
+
for (const row of rows) for (const key of Object.keys(row.fields)) fields.add(key);
|
|
177
|
+
const warnings = [];
|
|
178
|
+
for (const field of [...fields].sort()) {
|
|
179
|
+
const emptyCount = rows.filter((row) => isEmptyValue(row.fields[field])).length;
|
|
180
|
+
if (required.has(field)) {
|
|
181
|
+
if (emptyCount > 0) warnings.push({
|
|
182
|
+
field,
|
|
183
|
+
message: `${field} empty in ${emptyCount}/${rows.length} sampled`
|
|
184
|
+
});
|
|
185
|
+
} else if (emptyCount === rows.length) warnings.push({
|
|
186
|
+
field,
|
|
187
|
+
message: `${field} is empty in all ${rows.length} sampled rows`
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return warnings;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Zero-write preview: read ≤`limit` rows through the compiled view (a pure
|
|
194
|
+
* SELECT whose SQL already projects each row into its mapped fields) and
|
|
195
|
+
* compute sample warnings. Calls ONLY `store.readView` — never a write path —
|
|
196
|
+
* so it can never mutate the mirror. The distinct, write-one-row sibling is
|
|
197
|
+
* `runSync`'s sample mode (T13).
|
|
198
|
+
*/
|
|
199
|
+
async function previewSync(store, view, opts) {
|
|
200
|
+
const rows = await store.readView(view, { limit: opts.limit });
|
|
201
|
+
return {
|
|
202
|
+
rows,
|
|
203
|
+
sampledCount: rows.length,
|
|
204
|
+
warnings: computeSampleWarnings(rows, opts.requiredFields, opts.sourceTable)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
159
207
|
//#endregion
|
|
208
|
+
exports.computeSampleWarnings = computeSampleWarnings;
|
|
209
|
+
exports.previewSync = previewSync;
|
|
160
210
|
exports.reconcile = reconcile;
|
|
161
211
|
exports.runSync = runSync;
|
package/dist/sync-engine.d.cts
CHANGED
|
@@ -108,16 +108,23 @@ interface SyncStore {
|
|
|
108
108
|
/** Current mirror rows for a concept + role (including archived). */
|
|
109
109
|
listMirror(input: {
|
|
110
110
|
concept: string;
|
|
111
|
-
role: string;
|
|
111
|
+
role: string; /** Target custom-object type id, for `concept: "custom"` entities. */
|
|
112
|
+
objectTypeId?: string;
|
|
112
113
|
}): Promise<MirrorRecord[]>;
|
|
113
114
|
/** Apply the resolved actions to the mirror table for this concept + role. */
|
|
114
115
|
persist(input: {
|
|
115
116
|
concept: string;
|
|
116
|
-
role: string;
|
|
117
|
+
role: string; /** Target custom-object type id, for `concept: "custom"` entities. */
|
|
118
|
+
objectTypeId?: string;
|
|
117
119
|
actions: PersistAction[];
|
|
118
120
|
}): Promise<void>;
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Execute the view's SQL and return its rows. `opts.limit`, when given,
|
|
123
|
+
* caps the number of rows returned (used by `runSync`'s sample mode).
|
|
124
|
+
*/
|
|
125
|
+
readView(view: CompiledView, opts?: {
|
|
126
|
+
limit?: number;
|
|
127
|
+
}): Promise<SourceRow[]>;
|
|
121
128
|
/** Map host-side company refs → `crm_companies.id` (already-synced companies only). */
|
|
122
129
|
resolveCompanyRefs(refs: string[]): Promise<Map<string, string>>;
|
|
123
130
|
}
|
|
@@ -144,5 +151,33 @@ interface SyncResult {
|
|
|
144
151
|
* companies first. Returns per-entity and total action counts.
|
|
145
152
|
*/
|
|
146
153
|
declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
|
|
154
|
+
interface PreviewWarning {
|
|
155
|
+
field?: string;
|
|
156
|
+
message: string;
|
|
157
|
+
}
|
|
158
|
+
interface PreviewResult {
|
|
159
|
+
rows: SourceRow[];
|
|
160
|
+
sampledCount: number;
|
|
161
|
+
warnings: PreviewWarning[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Sample-scoped data-quality warnings over projected preview rows. Each mapped
|
|
165
|
+
* (or required-but-unmapped) field yields at most one warning, by precedence:
|
|
166
|
+
* a required field empty in ≥1 row → `K/N` form; an optional field empty in
|
|
167
|
+
* ALL rows → all-empty form; zero source rows → a single table-level warning.
|
|
168
|
+
*/
|
|
169
|
+
declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string[], sourceTable: string): PreviewWarning[];
|
|
170
|
+
/**
|
|
171
|
+
* Zero-write preview: read ≤`limit` rows through the compiled view (a pure
|
|
172
|
+
* SELECT whose SQL already projects each row into its mapped fields) and
|
|
173
|
+
* compute sample warnings. Calls ONLY `store.readView` — never a write path —
|
|
174
|
+
* so it can never mutate the mirror. The distinct, write-one-row sibling is
|
|
175
|
+
* `runSync`'s sample mode (T13).
|
|
176
|
+
*/
|
|
177
|
+
declare function previewSync(store: SyncStore, view: CompiledView, opts: {
|
|
178
|
+
limit: number;
|
|
179
|
+
requiredFields: string[];
|
|
180
|
+
sourceTable: string;
|
|
181
|
+
}): Promise<PreviewResult>;
|
|
147
182
|
//#endregion
|
|
148
|
-
export { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync };
|
|
183
|
+
export { EntitySyncResult, MirrorRecord, PersistAction, PreviewResult, PreviewWarning, SourceRow, SyncAction, SyncResult, SyncStore, computeSampleWarnings, previewSync, reconcile, runSync };
|
package/dist/sync-engine.d.mts
CHANGED
|
@@ -108,16 +108,23 @@ interface SyncStore {
|
|
|
108
108
|
/** Current mirror rows for a concept + role (including archived). */
|
|
109
109
|
listMirror(input: {
|
|
110
110
|
concept: string;
|
|
111
|
-
role: string;
|
|
111
|
+
role: string; /** Target custom-object type id, for `concept: "custom"` entities. */
|
|
112
|
+
objectTypeId?: string;
|
|
112
113
|
}): Promise<MirrorRecord[]>;
|
|
113
114
|
/** Apply the resolved actions to the mirror table for this concept + role. */
|
|
114
115
|
persist(input: {
|
|
115
116
|
concept: string;
|
|
116
|
-
role: string;
|
|
117
|
+
role: string; /** Target custom-object type id, for `concept: "custom"` entities. */
|
|
118
|
+
objectTypeId?: string;
|
|
117
119
|
actions: PersistAction[];
|
|
118
120
|
}): Promise<void>;
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Execute the view's SQL and return its rows. `opts.limit`, when given,
|
|
123
|
+
* caps the number of rows returned (used by `runSync`'s sample mode).
|
|
124
|
+
*/
|
|
125
|
+
readView(view: CompiledView, opts?: {
|
|
126
|
+
limit?: number;
|
|
127
|
+
}): Promise<SourceRow[]>;
|
|
121
128
|
/** Map host-side company refs → `crm_companies.id` (already-synced companies only). */
|
|
122
129
|
resolveCompanyRefs(refs: string[]): Promise<Map<string, string>>;
|
|
123
130
|
}
|
|
@@ -144,5 +151,33 @@ interface SyncResult {
|
|
|
144
151
|
* companies first. Returns per-entity and total action counts.
|
|
145
152
|
*/
|
|
146
153
|
declare function runSync(config: ClivlyEntitiesConfig, store: SyncStore, options?: CompileOptions): Promise<SyncResult>;
|
|
154
|
+
interface PreviewWarning {
|
|
155
|
+
field?: string;
|
|
156
|
+
message: string;
|
|
157
|
+
}
|
|
158
|
+
interface PreviewResult {
|
|
159
|
+
rows: SourceRow[];
|
|
160
|
+
sampledCount: number;
|
|
161
|
+
warnings: PreviewWarning[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Sample-scoped data-quality warnings over projected preview rows. Each mapped
|
|
165
|
+
* (or required-but-unmapped) field yields at most one warning, by precedence:
|
|
166
|
+
* a required field empty in ≥1 row → `K/N` form; an optional field empty in
|
|
167
|
+
* ALL rows → all-empty form; zero source rows → a single table-level warning.
|
|
168
|
+
*/
|
|
169
|
+
declare function computeSampleWarnings(rows: SourceRow[], requiredFields: string[], sourceTable: string): PreviewWarning[];
|
|
170
|
+
/**
|
|
171
|
+
* Zero-write preview: read ≤`limit` rows through the compiled view (a pure
|
|
172
|
+
* SELECT whose SQL already projects each row into its mapped fields) and
|
|
173
|
+
* compute sample warnings. Calls ONLY `store.readView` — never a write path —
|
|
174
|
+
* so it can never mutate the mirror. The distinct, write-one-row sibling is
|
|
175
|
+
* `runSync`'s sample mode (T13).
|
|
176
|
+
*/
|
|
177
|
+
declare function previewSync(store: SyncStore, view: CompiledView, opts: {
|
|
178
|
+
limit: number;
|
|
179
|
+
requiredFields: string[];
|
|
180
|
+
sourceTable: string;
|
|
181
|
+
}): Promise<PreviewResult>;
|
|
147
182
|
//#endregion
|
|
148
|
-
export { EntitySyncResult, MirrorRecord, PersistAction, SourceRow, SyncAction, SyncResult, SyncStore, reconcile, runSync };
|
|
183
|
+
export { EntitySyncResult, MirrorRecord, PersistAction, PreviewResult, PreviewWarning, SourceRow, SyncAction, SyncResult, SyncStore, computeSampleWarnings, previewSync, reconcile, runSync };
|
package/dist/sync-engine.mjs
CHANGED
|
@@ -113,7 +113,8 @@ function isCompositeEntity(config, entityKey) {
|
|
|
113
113
|
* companies first. Returns per-entity and total action counts.
|
|
114
114
|
*/
|
|
115
115
|
async function runSync(config, store, options = {}) {
|
|
116
|
-
const
|
|
116
|
+
const allViews = compileEntityViews(config, options).sort(syncOrder);
|
|
117
|
+
const views = options.sample ? allViews.filter((v) => v.entityKey === options.sample?.entityKey) : allViews;
|
|
117
118
|
const entities = [];
|
|
118
119
|
const totals = {
|
|
119
120
|
created: 0,
|
|
@@ -122,15 +123,18 @@ async function runSync(config, store, options = {}) {
|
|
|
122
123
|
archived: 0
|
|
123
124
|
};
|
|
124
125
|
for (const view of views) {
|
|
125
|
-
const
|
|
126
|
+
const objectTypeId = config.entities[view.entityKey]?.targetObjectTypeId;
|
|
127
|
+
const [source, mirror] = await Promise.all([store.readView(view, options.sample ? { limit: 1 } : void 0), store.listMirror({
|
|
126
128
|
concept: view.concept,
|
|
127
|
-
role: view.role
|
|
129
|
+
role: view.role,
|
|
130
|
+
objectTypeId
|
|
128
131
|
})]);
|
|
129
|
-
const actions = reconcile(source, mirror, { composite: isCompositeEntity(config, view.entityKey) });
|
|
132
|
+
const actions = reconcile(source, mirror, { composite: isCompositeEntity(config, view.entityKey) }).filter((a) => !(options.sample && a.kind === "archive"));
|
|
130
133
|
const persistActions = await resolveActions(store, actions);
|
|
131
134
|
await store.persist({
|
|
132
135
|
concept: view.concept,
|
|
133
136
|
role: view.role,
|
|
137
|
+
objectTypeId,
|
|
134
138
|
actions: persistActions
|
|
135
139
|
});
|
|
136
140
|
const counts = emptyTally();
|
|
@@ -155,5 +159,49 @@ async function runSync(config, store, options = {}) {
|
|
|
155
159
|
totals
|
|
156
160
|
};
|
|
157
161
|
}
|
|
162
|
+
function isEmptyValue(value) {
|
|
163
|
+
return value === null || value === void 0 || value === "";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Sample-scoped data-quality warnings over projected preview rows. Each mapped
|
|
167
|
+
* (or required-but-unmapped) field yields at most one warning, by precedence:
|
|
168
|
+
* a required field empty in ≥1 row → `K/N` form; an optional field empty in
|
|
169
|
+
* ALL rows → all-empty form; zero source rows → a single table-level warning.
|
|
170
|
+
*/
|
|
171
|
+
function computeSampleWarnings(rows, requiredFields, sourceTable) {
|
|
172
|
+
if (rows.length === 0) return [{ message: `no rows found in ${sourceTable}` }];
|
|
173
|
+
const required = new Set(requiredFields);
|
|
174
|
+
const fields = new Set(requiredFields);
|
|
175
|
+
for (const row of rows) for (const key of Object.keys(row.fields)) fields.add(key);
|
|
176
|
+
const warnings = [];
|
|
177
|
+
for (const field of [...fields].sort()) {
|
|
178
|
+
const emptyCount = rows.filter((row) => isEmptyValue(row.fields[field])).length;
|
|
179
|
+
if (required.has(field)) {
|
|
180
|
+
if (emptyCount > 0) warnings.push({
|
|
181
|
+
field,
|
|
182
|
+
message: `${field} empty in ${emptyCount}/${rows.length} sampled`
|
|
183
|
+
});
|
|
184
|
+
} else if (emptyCount === rows.length) warnings.push({
|
|
185
|
+
field,
|
|
186
|
+
message: `${field} is empty in all ${rows.length} sampled rows`
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return warnings;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Zero-write preview: read ≤`limit` rows through the compiled view (a pure
|
|
193
|
+
* SELECT whose SQL already projects each row into its mapped fields) and
|
|
194
|
+
* compute sample warnings. Calls ONLY `store.readView` — never a write path —
|
|
195
|
+
* so it can never mutate the mirror. The distinct, write-one-row sibling is
|
|
196
|
+
* `runSync`'s sample mode (T13).
|
|
197
|
+
*/
|
|
198
|
+
async function previewSync(store, view, opts) {
|
|
199
|
+
const rows = await store.readView(view, { limit: opts.limit });
|
|
200
|
+
return {
|
|
201
|
+
rows,
|
|
202
|
+
sampledCount: rows.length,
|
|
203
|
+
warnings: computeSampleWarnings(rows, opts.requiredFields, opts.sourceTable)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
158
206
|
//#endregion
|
|
159
|
-
export { reconcile, runSync };
|
|
207
|
+
export { computeSampleWarnings, previewSync, reconcile, runSync };
|
package/dist/types.d.cts
CHANGED
|
@@ -34,6 +34,7 @@ interface Deal {
|
|
|
34
34
|
name: string;
|
|
35
35
|
orgId: string;
|
|
36
36
|
owner: string | null;
|
|
37
|
+
position: number | null;
|
|
37
38
|
stage: DealStage;
|
|
38
39
|
value: number;
|
|
39
40
|
}
|
|
@@ -138,9 +139,11 @@ interface CreateActivityInput {
|
|
|
138
139
|
summary: string;
|
|
139
140
|
type: ActivityType;
|
|
140
141
|
}
|
|
142
|
+
type RecordSource = "all" | "synced";
|
|
141
143
|
interface ContactFilters {
|
|
142
144
|
companyId?: string;
|
|
143
145
|
search?: string;
|
|
146
|
+
source?: RecordSource;
|
|
144
147
|
status?: ContactStatus;
|
|
145
148
|
}
|
|
146
149
|
interface TaskFilters {
|
|
@@ -153,4 +156,4 @@ interface TaskFilters {
|
|
|
153
156
|
status?: TaskStatus;
|
|
154
157
|
}
|
|
155
158
|
//#endregion
|
|
156
|
-
export { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput };
|
|
159
|
+
export { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, RecordSource, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput };
|
package/dist/types.d.mts
CHANGED
|
@@ -34,6 +34,7 @@ interface Deal {
|
|
|
34
34
|
name: string;
|
|
35
35
|
orgId: string;
|
|
36
36
|
owner: string | null;
|
|
37
|
+
position: number | null;
|
|
37
38
|
stage: DealStage;
|
|
38
39
|
value: number;
|
|
39
40
|
}
|
|
@@ -138,9 +139,11 @@ interface CreateActivityInput {
|
|
|
138
139
|
summary: string;
|
|
139
140
|
type: ActivityType;
|
|
140
141
|
}
|
|
142
|
+
type RecordSource = "all" | "synced";
|
|
141
143
|
interface ContactFilters {
|
|
142
144
|
companyId?: string;
|
|
143
145
|
search?: string;
|
|
146
|
+
source?: RecordSource;
|
|
144
147
|
status?: ContactStatus;
|
|
145
148
|
}
|
|
146
149
|
interface TaskFilters {
|
|
@@ -153,4 +156,4 @@ interface TaskFilters {
|
|
|
153
156
|
status?: TaskStatus;
|
|
154
157
|
}
|
|
155
158
|
//#endregion
|
|
156
|
-
export { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput };
|
|
159
|
+
export { Activity, ActivityType, Company, Contact, ContactFilters, ContactStatus, CreateActivityInput, CreateCompanyInput, CreateContactInput, CreateDealInput, CreateTaskInput, CrmRole, Deal, DealStage, Note, RecordSource, Task, TaskEntityType, TaskFilters, TaskPriority, TaskStatus, UpdateContactInput, UpdateDealInput, UpdateTaskInput };
|
package/dist/view-compiler.d.cts
CHANGED
|
@@ -11,6 +11,13 @@ interface CompileOptions {
|
|
|
11
11
|
column: string;
|
|
12
12
|
value: string | number;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Restrict `runSync` to a single entity's view, for a "test with one
|
|
16
|
+
* record" dry run. See `runSync`'s `sample` option in `sync-engine.ts`.
|
|
17
|
+
*/
|
|
18
|
+
sample?: {
|
|
19
|
+
entityKey: string;
|
|
20
|
+
};
|
|
14
21
|
/**
|
|
15
22
|
* Emit views `WITH (security_invoker = true)` so they run with the querying
|
|
16
23
|
* role's privileges instead of the (owner's) definer privileges — the
|
package/dist/view-compiler.d.mts
CHANGED
|
@@ -11,6 +11,13 @@ interface CompileOptions {
|
|
|
11
11
|
column: string;
|
|
12
12
|
value: string | number;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Restrict `runSync` to a single entity's view, for a "test with one
|
|
16
|
+
* record" dry run. See `runSync`'s `sample` option in `sync-engine.ts`.
|
|
17
|
+
*/
|
|
18
|
+
sample?: {
|
|
19
|
+
entityKey: string;
|
|
20
|
+
};
|
|
14
21
|
/**
|
|
15
22
|
* Emit views `WITH (security_invoker = true)` so they run with the querying
|
|
16
23
|
* role's privileges instead of the (owner's) definer privileges — the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clivly/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-next.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ORM/auth-agnostic CRM adapter contracts and shared domain types for Clivly.",
|
|
@@ -37,6 +37,16 @@
|
|
|
37
37
|
"default": "./dist/entity-heuristics.cjs"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
+
"./mapping-score": {
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/mapping-score.d.mts",
|
|
43
|
+
"default": "./dist/mapping-score.mjs"
|
|
44
|
+
},
|
|
45
|
+
"require": {
|
|
46
|
+
"types": "./dist/mapping-score.d.cts",
|
|
47
|
+
"default": "./dist/mapping-score.cjs"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
40
50
|
"./auth-adapter": {
|
|
41
51
|
"import": {
|
|
42
52
|
"types": "./dist/auth-adapter.d.mts",
|
|
@@ -139,7 +149,7 @@
|
|
|
139
149
|
"drizzle-orm": "^0.45.1",
|
|
140
150
|
"tsdown": "^0.21.9",
|
|
141
151
|
"typescript": "^6",
|
|
142
|
-
"vitest": "
|
|
152
|
+
"vitest": "3.2.7"
|
|
143
153
|
},
|
|
144
154
|
"scripts": {
|
|
145
155
|
"build": "tsdown",
|