@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.
Files changed (42) hide show
  1. package/README.md +96 -15
  2. package/dist/drizzle.cjs +82 -1
  3. package/dist/drizzle.d.cts +23 -2
  4. package/dist/drizzle.d.mts +23 -2
  5. package/dist/drizzle.mjs +81 -2
  6. package/dist/entity-config-PoOGzvpN.cjs +728 -0
  7. package/dist/entity-config-Tb5bbKLj.mjs +621 -0
  8. package/dist/entity-config.cjs +12 -349
  9. package/dist/entity-config.d.cts +120 -1
  10. package/dist/entity-config.d.mts +120 -1
  11. package/dist/entity-config.mjs +1 -338
  12. package/dist/entity-heuristics.cjs +28 -8
  13. package/dist/entity-heuristics.d.cts +2 -12
  14. package/dist/entity-heuristics.d.mts +2 -12
  15. package/dist/entity-heuristics.mjs +28 -8
  16. package/dist/env-file.cjs +59 -0
  17. package/dist/env-file.d.cts +23 -0
  18. package/dist/env-file.d.mts +23 -0
  19. package/dist/env-file.mjs +55 -0
  20. package/dist/index.cjs +83 -2
  21. package/dist/index.d.cts +73 -3
  22. package/dist/index.d.mts +73 -3
  23. package/dist/index.mjs +69 -3
  24. package/dist/integration-state.cjs +242 -0
  25. package/dist/integration-state.d.cts +88 -0
  26. package/dist/integration-state.d.mts +88 -0
  27. package/dist/integration-state.mjs +232 -0
  28. package/dist/mappings-config-B0W5bVZj.d.mts +168 -0
  29. package/dist/mappings-config-BWo4cRDF.d.cts +168 -0
  30. package/dist/mappings-config.cjs +32 -6
  31. package/dist/mappings-config.d.cts +1 -40
  32. package/dist/mappings-config.d.mts +1 -40
  33. package/dist/mappings-config.mjs +32 -6
  34. package/dist/projection-BXBkh7qp.cjs +98 -0
  35. package/dist/projection-C_EMRmZ8.mjs +75 -0
  36. package/dist/sync-engine.cjs +9 -2
  37. package/dist/sync-engine.mjs +9 -2
  38. package/dist/view-compiler-BKrBA_II.mjs +254 -0
  39. package/dist/view-compiler-BwZ9UgLp.cjs +277 -0
  40. package/dist/view-compiler.cjs +5 -188
  41. package/dist/view-compiler.mjs +1 -184
  42. package/package.json +21 -1
@@ -1,189 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/view-compiler.ts
3
- /**
4
- * View compiler (Phase 2) — turns a validated entity config into read-only
5
- * Postgres view DDL.
6
- *
7
- * Each entity compiles to `crm_src_<entity>`: a projection over the host table
8
- * that (a) selects the source primary key as `id` (the sync engine's
9
- * `source_ref`), (b) aliases mapped columns to their Clivly field names,
10
- * (c) resolves each relationship to a `<relationship>_ref` column carrying the
11
- * host-side key, and (d) applies the entity's filter as a WHERE clause.
12
- *
13
- * The view is the security boundary: it exposes only allow-listed columns, and
14
- * the sync engine (Phase 3) reads exclusively from it — never the raw table.
15
- * Relationship refs stay host-side keys here; Phase 3 maps `<rel>_ref` onto the
16
- * corresponding `crm_companies.id` mirror row.
17
- *
18
- * Pure string generation — no DB access. Config is developer-authored, but all
19
- * identifiers and literals are still escaped so output is deterministic and
20
- * injection-safe.
21
- */
22
- /** The source primary-key column. Assumed `id`; the sync engine reads it as `source_ref`. */
23
- const SOURCE_KEY = "id";
24
- const DEFAULT_VIEW_PREFIX = "crm_src_";
25
- const DEFAULT_ROLE = "primary";
26
- /** Quote a single identifier part, escaping embedded double-quotes. */
27
- function quoteIdent(name) {
28
- return `"${name.replace(/"/g, "\"\"")}"`;
29
- }
30
- /** Emit a SQL literal for a scalar filter value. */
31
- function quoteLiteral(value) {
32
- if (typeof value === "number") return String(value);
33
- if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
34
- return `'${value.replace(/'/g, "''")}'`;
35
- }
36
- function quoteList(values) {
37
- return `(${values.map((v) => quoteLiteral(v)).join(", ")})`;
38
- }
39
- /** Compile one column predicate into a SQL boolean expression. */
40
- function compilePredicate(table, column, value) {
41
- const col = `${quoteIdent(table)}.${quoteIdent(column)}`;
42
- if (value === null || typeof value !== "object") return `${col} = ${quoteLiteral(value)}`;
43
- if ("eq" in value) return `${col} = ${quoteLiteral(value.eq)}`;
44
- if ("ne" in value) return `${col} <> ${quoteLiteral(value.ne)}`;
45
- if ("in" in value) return `${col} IN ${quoteList(value.in)}`;
46
- if ("notIn" in value) return `${col} NOT IN ${quoteList(value.notIn)}`;
47
- if ("isNull" in value) return `${col} IS NULL`;
48
- if ("isNotNull" in value) return `${col} IS NOT NULL`;
49
- throw new Error(`unsupported filter predicate on ${table}.${column}`);
50
- }
51
- /**
52
- * Build the WHERE conditions for an entity. Returns an array of SQL fragments
53
- * (ANDed by the caller). `$raw` filters pass through verbatim.
54
- */
55
- function compileFilterConditions(table, filter) {
56
- if (!filter) return [];
57
- if ("$raw" in filter) return [`(${filter.$raw})`];
58
- return Object.entries(filter).map(([column, value]) => compilePredicate(table, column, value));
59
- }
60
- /** Split a `table.column` ref, defaulting the table when unqualified. */
61
- function refParts(ref, defaultTable) {
62
- const dot = ref.indexOf(".");
63
- if (dot === -1) return {
64
- table: defaultTable,
65
- column: ref
66
- };
67
- return {
68
- table: ref.slice(0, dot),
69
- column: ref.slice(dot + 1)
70
- };
71
- }
72
- function compileRelationship(relName, rel, source, related) {
73
- const refCol = `${relName}_ref`;
74
- const refAlias = quoteIdent(refCol);
75
- const { via } = rel;
76
- if ("fkOn" in via) {
77
- const relatedTable = related?.source ?? "";
78
- if (via.fkOn === "company") {
79
- const { table, column } = refParts(via.column, relatedTable);
80
- const relatedFilter = compileFilterConditions(table, related?.filter);
81
- const on = [`${quoteIdent(table)}.${quoteIdent(column)} = ${quoteIdent(source.source)}.${quoteIdent(SOURCE_KEY)}`, ...relatedFilter].join(" AND ");
82
- return {
83
- clause: `LEFT JOIN ${quoteIdent(table)} ON ${on}`,
84
- refSelect: `${quoteIdent(table)}.${quoteIdent(SOURCE_KEY)} AS ${refAlias}`,
85
- refColumn: refCol
86
- };
87
- }
88
- const { table, column } = refParts(via.column, source.source);
89
- const on = [`${quoteIdent(relatedTable)}.${quoteIdent(SOURCE_KEY)} = ${quoteIdent(table)}.${quoteIdent(column)}`, ...compileFilterConditions(relatedTable, related?.filter)].join(" AND ");
90
- return {
91
- clause: `LEFT JOIN ${quoteIdent(relatedTable)} ON ${on}`,
92
- refSelect: `${quoteIdent(relatedTable)}.${quoteIdent(SOURCE_KEY)} AS ${refAlias}`,
93
- refColumn: refCol
94
- };
95
- }
96
- if ("through" in via) {
97
- const on = `${quoteIdent(via.through)}.${quoteIdent(via.localKey)} = ${quoteIdent(source.source)}.${quoteIdent(SOURCE_KEY)}`;
98
- return {
99
- clause: `LEFT JOIN ${quoteIdent(via.through)} ON ${on}`,
100
- refSelect: `${quoteIdent(via.through)}.${quoteIdent(via.foreignKey)} AS ${refAlias}`,
101
- refColumn: refCol
102
- };
103
- }
104
- return {
105
- clause: via.$raw,
106
- refSelect: null,
107
- refColumn: refCol
108
- };
109
- }
110
- function compileEntityView(config, entityKey, options = {}) {
111
- const entity = config.entities[entityKey];
112
- if (!entity) throw new Error(`entity "${entityKey}" is not defined`);
113
- const viewName = `${options.viewPrefix ?? "crm_src_"}${entityKey}`;
114
- const src = entity.source;
115
- const selectItems = [`${quoteIdent(src)}.${quoteIdent(SOURCE_KEY)} AS ${quoteIdent("id")}`];
116
- const columns = ["id"];
117
- for (const [field, column] of Object.entries(entity.fields)) {
118
- selectItems.push(`${quoteIdent(src)}.${quoteIdent(column)} AS ${quoteIdent(field)}`);
119
- columns.push(field);
120
- }
121
- const joins = [];
122
- for (const [relName, rel] of Object.entries(entity.relationships ?? {})) {
123
- const compiled = compileRelationship(relName, rel, entity, config.entities[rel.entity]);
124
- joins.push(compiled.clause);
125
- if (compiled.refSelect) {
126
- selectItems.push(compiled.refSelect);
127
- columns.push(compiled.refColumn);
128
- }
129
- }
130
- const conditions = compileFilterConditions(src, entity.filter);
131
- if (options.orgScope) conditions.push(`${quoteIdent(src)}.${quoteIdent(options.orgScope.column)} = ${quoteLiteral(options.orgScope.value)}`);
132
- const lines = [
133
- options.securityInvoker ?? true ? `CREATE OR REPLACE VIEW ${quoteIdent(viewName)} WITH (security_invoker = true) AS` : `CREATE OR REPLACE VIEW ${quoteIdent(viewName)} AS`,
134
- `SELECT ${selectItems.join(", ")}`,
135
- `FROM ${quoteIdent(src)}`,
136
- ...joins
137
- ];
138
- if (conditions.length > 0) lines.push(`WHERE ${conditions.join(" AND ")}`);
139
- const sql = `${lines.join("\n")};`;
140
- return {
141
- entityKey,
142
- viewName,
143
- concept: entity.concept,
144
- role: entity.role ?? DEFAULT_ROLE,
145
- sourceTable: src,
146
- columns,
147
- sql,
148
- dropSql: `DROP VIEW IF EXISTS ${quoteIdent(viewName)};`
149
- };
150
- }
151
- /** Compile every entity in the config into its view, in declaration order. */
152
- function compileEntityViews(config, options = {}) {
153
- return Object.keys(config.entities).map((key) => compileEntityView(config, key, options));
154
- }
155
- /**
156
- * Dry-run each entity's compiled view against the host DB via a caller-provided
157
- * `run` function, so `$raw` filter/join fragments — which skip schema validation
158
- * (see `validateEntitiesConfig`) — fail loudly *before* the first sync instead of
159
- * at sync time. Never throws: each view's outcome is collected into an
160
- * `ExplainResult`, so a mapping UI or CLI can show every problem at once.
161
- *
162
- * `run` should execute the SQL somewhere side-effect-free — e.g. inside a
163
- * transaction it rolls back, or by `EXPLAIN`-ing the statement — since the
164
- * compiled SQL is a `CREATE OR REPLACE VIEW`.
165
- */
166
- async function explainEntitiesConfig(config, run, options = {}) {
167
- const results = [];
168
- for (const view of compileEntityViews(config, options)) try {
169
- await run(view.sql);
170
- results.push({
171
- entityKey: view.entityKey,
172
- ok: true,
173
- sql: view.sql
174
- });
175
- } catch (error) {
176
- results.push({
177
- entityKey: view.entityKey,
178
- ok: false,
179
- sql: view.sql,
180
- error: error instanceof Error ? error.message : String(error)
181
- });
182
- }
183
- return results;
184
- }
185
- //#endregion
186
- exports.DEFAULT_VIEW_PREFIX = DEFAULT_VIEW_PREFIX;
187
- exports.compileEntityView = compileEntityView;
188
- exports.compileEntityViews = compileEntityViews;
189
- exports.explainEntitiesConfig = explainEntitiesConfig;
2
+ const require_view_compiler = require("./view-compiler-BwZ9UgLp.cjs");
3
+ exports.DEFAULT_VIEW_PREFIX = require_view_compiler.DEFAULT_VIEW_PREFIX;
4
+ exports.compileEntityView = require_view_compiler.compileEntityView;
5
+ exports.compileEntityViews = require_view_compiler.compileEntityViews;
6
+ exports.explainEntitiesConfig = require_view_compiler.explainEntitiesConfig;
@@ -1,185 +1,2 @@
1
- //#region src/view-compiler.ts
2
- /**
3
- * View compiler (Phase 2) — turns a validated entity config into read-only
4
- * Postgres view DDL.
5
- *
6
- * Each entity compiles to `crm_src_<entity>`: a projection over the host table
7
- * that (a) selects the source primary key as `id` (the sync engine's
8
- * `source_ref`), (b) aliases mapped columns to their Clivly field names,
9
- * (c) resolves each relationship to a `<relationship>_ref` column carrying the
10
- * host-side key, and (d) applies the entity's filter as a WHERE clause.
11
- *
12
- * The view is the security boundary: it exposes only allow-listed columns, and
13
- * the sync engine (Phase 3) reads exclusively from it — never the raw table.
14
- * Relationship refs stay host-side keys here; Phase 3 maps `<rel>_ref` onto the
15
- * corresponding `crm_companies.id` mirror row.
16
- *
17
- * Pure string generation — no DB access. Config is developer-authored, but all
18
- * identifiers and literals are still escaped so output is deterministic and
19
- * injection-safe.
20
- */
21
- /** The source primary-key column. Assumed `id`; the sync engine reads it as `source_ref`. */
22
- const SOURCE_KEY = "id";
23
- const DEFAULT_VIEW_PREFIX = "crm_src_";
24
- const DEFAULT_ROLE = "primary";
25
- /** Quote a single identifier part, escaping embedded double-quotes. */
26
- function quoteIdent(name) {
27
- return `"${name.replace(/"/g, "\"\"")}"`;
28
- }
29
- /** Emit a SQL literal for a scalar filter value. */
30
- function quoteLiteral(value) {
31
- if (typeof value === "number") return String(value);
32
- if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
33
- return `'${value.replace(/'/g, "''")}'`;
34
- }
35
- function quoteList(values) {
36
- return `(${values.map((v) => quoteLiteral(v)).join(", ")})`;
37
- }
38
- /** Compile one column predicate into a SQL boolean expression. */
39
- function compilePredicate(table, column, value) {
40
- const col = `${quoteIdent(table)}.${quoteIdent(column)}`;
41
- if (value === null || typeof value !== "object") return `${col} = ${quoteLiteral(value)}`;
42
- if ("eq" in value) return `${col} = ${quoteLiteral(value.eq)}`;
43
- if ("ne" in value) return `${col} <> ${quoteLiteral(value.ne)}`;
44
- if ("in" in value) return `${col} IN ${quoteList(value.in)}`;
45
- if ("notIn" in value) return `${col} NOT IN ${quoteList(value.notIn)}`;
46
- if ("isNull" in value) return `${col} IS NULL`;
47
- if ("isNotNull" in value) return `${col} IS NOT NULL`;
48
- throw new Error(`unsupported filter predicate on ${table}.${column}`);
49
- }
50
- /**
51
- * Build the WHERE conditions for an entity. Returns an array of SQL fragments
52
- * (ANDed by the caller). `$raw` filters pass through verbatim.
53
- */
54
- function compileFilterConditions(table, filter) {
55
- if (!filter) return [];
56
- if ("$raw" in filter) return [`(${filter.$raw})`];
57
- return Object.entries(filter).map(([column, value]) => compilePredicate(table, column, value));
58
- }
59
- /** Split a `table.column` ref, defaulting the table when unqualified. */
60
- function refParts(ref, defaultTable) {
61
- const dot = ref.indexOf(".");
62
- if (dot === -1) return {
63
- table: defaultTable,
64
- column: ref
65
- };
66
- return {
67
- table: ref.slice(0, dot),
68
- column: ref.slice(dot + 1)
69
- };
70
- }
71
- function compileRelationship(relName, rel, source, related) {
72
- const refCol = `${relName}_ref`;
73
- const refAlias = quoteIdent(refCol);
74
- const { via } = rel;
75
- if ("fkOn" in via) {
76
- const relatedTable = related?.source ?? "";
77
- if (via.fkOn === "company") {
78
- const { table, column } = refParts(via.column, relatedTable);
79
- const relatedFilter = compileFilterConditions(table, related?.filter);
80
- const on = [`${quoteIdent(table)}.${quoteIdent(column)} = ${quoteIdent(source.source)}.${quoteIdent(SOURCE_KEY)}`, ...relatedFilter].join(" AND ");
81
- return {
82
- clause: `LEFT JOIN ${quoteIdent(table)} ON ${on}`,
83
- refSelect: `${quoteIdent(table)}.${quoteIdent(SOURCE_KEY)} AS ${refAlias}`,
84
- refColumn: refCol
85
- };
86
- }
87
- const { table, column } = refParts(via.column, source.source);
88
- const on = [`${quoteIdent(relatedTable)}.${quoteIdent(SOURCE_KEY)} = ${quoteIdent(table)}.${quoteIdent(column)}`, ...compileFilterConditions(relatedTable, related?.filter)].join(" AND ");
89
- return {
90
- clause: `LEFT JOIN ${quoteIdent(relatedTable)} ON ${on}`,
91
- refSelect: `${quoteIdent(relatedTable)}.${quoteIdent(SOURCE_KEY)} AS ${refAlias}`,
92
- refColumn: refCol
93
- };
94
- }
95
- if ("through" in via) {
96
- const on = `${quoteIdent(via.through)}.${quoteIdent(via.localKey)} = ${quoteIdent(source.source)}.${quoteIdent(SOURCE_KEY)}`;
97
- return {
98
- clause: `LEFT JOIN ${quoteIdent(via.through)} ON ${on}`,
99
- refSelect: `${quoteIdent(via.through)}.${quoteIdent(via.foreignKey)} AS ${refAlias}`,
100
- refColumn: refCol
101
- };
102
- }
103
- return {
104
- clause: via.$raw,
105
- refSelect: null,
106
- refColumn: refCol
107
- };
108
- }
109
- function compileEntityView(config, entityKey, options = {}) {
110
- const entity = config.entities[entityKey];
111
- if (!entity) throw new Error(`entity "${entityKey}" is not defined`);
112
- const viewName = `${options.viewPrefix ?? "crm_src_"}${entityKey}`;
113
- const src = entity.source;
114
- const selectItems = [`${quoteIdent(src)}.${quoteIdent(SOURCE_KEY)} AS ${quoteIdent("id")}`];
115
- const columns = ["id"];
116
- for (const [field, column] of Object.entries(entity.fields)) {
117
- selectItems.push(`${quoteIdent(src)}.${quoteIdent(column)} AS ${quoteIdent(field)}`);
118
- columns.push(field);
119
- }
120
- const joins = [];
121
- for (const [relName, rel] of Object.entries(entity.relationships ?? {})) {
122
- const compiled = compileRelationship(relName, rel, entity, config.entities[rel.entity]);
123
- joins.push(compiled.clause);
124
- if (compiled.refSelect) {
125
- selectItems.push(compiled.refSelect);
126
- columns.push(compiled.refColumn);
127
- }
128
- }
129
- const conditions = compileFilterConditions(src, entity.filter);
130
- if (options.orgScope) conditions.push(`${quoteIdent(src)}.${quoteIdent(options.orgScope.column)} = ${quoteLiteral(options.orgScope.value)}`);
131
- const lines = [
132
- options.securityInvoker ?? true ? `CREATE OR REPLACE VIEW ${quoteIdent(viewName)} WITH (security_invoker = true) AS` : `CREATE OR REPLACE VIEW ${quoteIdent(viewName)} AS`,
133
- `SELECT ${selectItems.join(", ")}`,
134
- `FROM ${quoteIdent(src)}`,
135
- ...joins
136
- ];
137
- if (conditions.length > 0) lines.push(`WHERE ${conditions.join(" AND ")}`);
138
- const sql = `${lines.join("\n")};`;
139
- return {
140
- entityKey,
141
- viewName,
142
- concept: entity.concept,
143
- role: entity.role ?? DEFAULT_ROLE,
144
- sourceTable: src,
145
- columns,
146
- sql,
147
- dropSql: `DROP VIEW IF EXISTS ${quoteIdent(viewName)};`
148
- };
149
- }
150
- /** Compile every entity in the config into its view, in declaration order. */
151
- function compileEntityViews(config, options = {}) {
152
- return Object.keys(config.entities).map((key) => compileEntityView(config, key, options));
153
- }
154
- /**
155
- * Dry-run each entity's compiled view against the host DB via a caller-provided
156
- * `run` function, so `$raw` filter/join fragments — which skip schema validation
157
- * (see `validateEntitiesConfig`) — fail loudly *before* the first sync instead of
158
- * at sync time. Never throws: each view's outcome is collected into an
159
- * `ExplainResult`, so a mapping UI or CLI can show every problem at once.
160
- *
161
- * `run` should execute the SQL somewhere side-effect-free — e.g. inside a
162
- * transaction it rolls back, or by `EXPLAIN`-ing the statement — since the
163
- * compiled SQL is a `CREATE OR REPLACE VIEW`.
164
- */
165
- async function explainEntitiesConfig(config, run, options = {}) {
166
- const results = [];
167
- for (const view of compileEntityViews(config, options)) try {
168
- await run(view.sql);
169
- results.push({
170
- entityKey: view.entityKey,
171
- ok: true,
172
- sql: view.sql
173
- });
174
- } catch (error) {
175
- results.push({
176
- entityKey: view.entityKey,
177
- ok: false,
178
- sql: view.sql,
179
- error: error instanceof Error ? error.message : String(error)
180
- });
181
- }
182
- return results;
183
- }
184
- //#endregion
1
+ import { i as explainEntitiesConfig, n as compileEntityView, r as compileEntityViews, t as DEFAULT_VIEW_PREFIX } from "./view-compiler-BKrBA_II.mjs";
185
2
  export { DEFAULT_VIEW_PREFIX, compileEntityView, compileEntityViews, explainEntitiesConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ORM/auth-agnostic CRM adapter contracts and shared domain types for Clivly.",
@@ -47,6 +47,26 @@
47
47
  "default": "./dist/mapping-score.cjs"
48
48
  }
49
49
  },
50
+ "./integration-state": {
51
+ "import": {
52
+ "types": "./dist/integration-state.d.mts",
53
+ "default": "./dist/integration-state.mjs"
54
+ },
55
+ "require": {
56
+ "types": "./dist/integration-state.d.cts",
57
+ "default": "./dist/integration-state.cjs"
58
+ }
59
+ },
60
+ "./env-file": {
61
+ "import": {
62
+ "types": "./dist/env-file.d.mts",
63
+ "default": "./dist/env-file.mjs"
64
+ },
65
+ "require": {
66
+ "types": "./dist/env-file.d.cts",
67
+ "default": "./dist/env-file.cjs"
68
+ }
69
+ },
50
70
  "./auth-adapter": {
51
71
  "import": {
52
72
  "types": "./dist/auth-adapter.d.mts",