clivly 0.1.0 → 0.2.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/README.md +63 -3
- package/adapter-nLfZpgIG.d.cts +40 -0
- package/adapter-xs04z0KX.d.mts +40 -0
- package/auth-adapter-CBOKDhlc.cjs +11 -0
- package/auth-adapter-FEOR1nx-.d.mts +30 -0
- package/auth-adapter-wqnnEjo3.mjs +6 -0
- package/auth-adapter-wvfGKsOJ.d.cts +30 -0
- package/auth-better-auth.cjs +28 -0
- package/auth-better-auth.d.cts +33 -0
- package/auth-better-auth.d.mts +33 -0
- package/auth-better-auth.mjs +26 -0
- package/auth-clerk.cjs +34 -0
- package/auth-clerk.d.cts +32 -0
- package/auth-clerk.d.mts +32 -0
- package/auth-clerk.mjs +32 -0
- package/auth-workos.cjs +47 -0
- package/auth-workos.d.cts +30 -0
- package/auth-workos.d.mts +30 -0
- package/auth-workos.mjs +45 -0
- package/core-adapter.cjs +0 -0
- package/core-adapter.d.cts +2 -0
- package/core-adapter.d.mts +2 -0
- package/core-adapter.mjs +1 -0
- package/core-auth-adapter.cjs +3 -0
- package/core-auth-adapter.d.cts +2 -0
- package/core-auth-adapter.d.mts +2 -0
- package/core-auth-adapter.mjs +2 -0
- package/core-drizzle.cjs +32 -0
- package/core-drizzle.d.cts +21 -0
- package/core-drizzle.d.mts +21 -0
- package/core-drizzle.mjs +31 -0
- package/core-entity-config.cjs +13 -0
- package/core-entity-config.d.cts +2 -0
- package/core-entity-config.d.mts +2 -0
- package/core-entity-config.mjs +2 -0
- package/core-entity-heuristics.cjs +442 -0
- package/core-entity-heuristics.d.cts +68 -0
- package/core-entity-heuristics.d.mts +68 -0
- package/core-entity-heuristics.mjs +431 -0
- package/core-mapping-form.cjs +5 -0
- package/core-mapping-form.d.cts +2 -0
- package/core-mapping-form.d.mts +2 -0
- package/core-mapping-form.mjs +2 -0
- package/core-mapping-score.cjs +12 -0
- package/core-mapping-score.d.cts +2 -0
- package/core-mapping-score.d.mts +2 -0
- package/core-mapping-score.mjs +2 -0
- package/core-mappings-config.cjs +3 -0
- package/core-mappings-config.d.cts +2 -0
- package/core-mappings-config.d.mts +2 -0
- package/core-mappings-config.mjs +2 -0
- package/core-sync-engine.cjs +6 -0
- package/core-sync-engine.d.cts +2 -0
- package/core-sync-engine.d.mts +2 -0
- package/core-sync-engine.mjs +2 -0
- package/core-types.cjs +0 -0
- package/core-types.d.cts +2 -0
- package/core-types.d.mts +2 -0
- package/core-types.mjs +1 -0
- package/core-view-compiler.cjs +5 -0
- package/core-view-compiler.d.cts +2 -0
- package/core-view-compiler.d.mts +2 -0
- package/core-view-compiler.mjs +2 -0
- package/core.cjs +27 -0
- package/core.d.cts +9 -0
- package/core.d.mts +9 -0
- package/core.mjs +7 -0
- package/drizzle-meta-BGn0BqGb.cjs +9 -0
- package/drizzle-meta-CnC336L-.mjs +4 -0
- package/drizzle.cjs +107 -0
- package/drizzle.d.cts +28 -0
- package/drizzle.d.mts +28 -0
- package/drizzle.mjs +105 -0
- package/entity-config-Cvjo1cpv.cjs +4598 -0
- package/entity-config-D1DFyeUp.d.mts +2403 -0
- package/entity-config-DoR0khce.mjs +4533 -0
- package/entity-config-GczjD3y8.d.cts +2403 -0
- package/index.cjs +926 -0
- package/index.d.cts +56 -0
- package/index.d.mts +56 -0
- package/index.mjs +925 -0
- package/mapping-form-BHtPexzI.mjs +47 -0
- package/mapping-form-CUlFVfG8.d.cts +35 -0
- package/mapping-form-DdmDebC3.cjs +64 -0
- package/mapping-form-kjSiJaCX.d.mts +35 -0
- package/mapping-score-A2lRnlpk.d.mts +49 -0
- package/mapping-score-BJNe0lFn.mjs +87 -0
- package/mapping-score-BcjC0ZQs.d.cts +49 -0
- package/mapping-score-D9gfhtRu.cjs +146 -0
- package/mappings-config-CYPrzB3K.mjs +99 -0
- package/mappings-config-DTu_2cyx.cjs +104 -0
- package/mappings-config-DjRLCb2Q.d.cts +42 -0
- package/mappings-config-GxiYsSBQ.d.mts +42 -0
- package/namespace-probe-BtS0nXcr.d.cts +299 -0
- package/namespace-probe-CYMie-BI.d.mts +299 -0
- package/package.json +224 -21
- package/sdk-BZUn6wzs.mjs +764 -0
- package/sdk-CP_mYZa5.cjs +793 -0
- package/sdk.cjs +7 -0
- package/sdk.d.cts +33 -0
- package/sdk.d.mts +33 -0
- package/sdk.mjs +2 -0
- package/sync-engine-DUc0qWgo.cjs +230 -0
- package/sync-engine-DWnQPjV3.d.mts +183 -0
- package/sync-engine-DedXngop.mjs +207 -0
- package/sync-engine-uhfzKM-e.d.cts +183 -0
- package/types-C4POUcvk.d.mts +158 -0
- package/types-LJTBhkcN.d.cts +158 -0
- package/view-compiler-B4UYTvRM.d.cts +71 -0
- package/view-compiler-BHzbI9b7.cjs +202 -0
- package/view-compiler-CosFGar8.mjs +185 -0
- package/view-compiler-i_xFf-L0.d.mts +71 -0
- package/vite.cjs +38 -0
- package/vite.d.cts +23 -0
- package/vite.d.mts +23 -0
- package/vite.mjs +37 -0
- package/dist/index.mjs +0 -193
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { a as ClivlyEntitiesConfig } from "./entity-config-GczjD3y8.cjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/view-compiler.d.ts
|
|
4
|
+
interface CompileOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Optional tenant-scoping predicate ANDed into every view's WHERE, e.g.
|
|
7
|
+
* `{ column: "workspace_id", value: "ws_123" }`. Column must exist on the
|
|
8
|
+
* entity's source table (the compiler does not verify that — validate first).
|
|
9
|
+
*/
|
|
10
|
+
orgScope?: {
|
|
11
|
+
column: string;
|
|
12
|
+
value: string | number;
|
|
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
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Emit views `WITH (security_invoker = true)` so they run with the querying
|
|
23
|
+
* role's privileges instead of the (owner's) definer privileges — the
|
|
24
|
+
* default Postgres behaviour, which bypasses the caller's RLS. Defaults to
|
|
25
|
+
* `true`: the security boundary the product promises. Set `false` only for
|
|
26
|
+
* hosts on Postgres < 15, which lacks the option.
|
|
27
|
+
*/
|
|
28
|
+
securityInvoker?: boolean;
|
|
29
|
+
/** View name prefix. Defaults to `crm_src_`. */
|
|
30
|
+
viewPrefix?: string;
|
|
31
|
+
}
|
|
32
|
+
interface CompiledView {
|
|
33
|
+
/** Output column names in SELECT order (`id`, mapped fields, then `<rel>_ref`s). */
|
|
34
|
+
columns: string[];
|
|
35
|
+
concept: string;
|
|
36
|
+
/** `DROP VIEW IF EXISTS …` statement (for teardown / reconfiguration). */
|
|
37
|
+
dropSql: string;
|
|
38
|
+
entityKey: string;
|
|
39
|
+
/** Resolved role — the entity's `role` or `"primary"`. */
|
|
40
|
+
role: string;
|
|
41
|
+
sourceTable: string;
|
|
42
|
+
/** `CREATE OR REPLACE VIEW … AS …` statement. */
|
|
43
|
+
sql: string;
|
|
44
|
+
viewName: string;
|
|
45
|
+
}
|
|
46
|
+
declare function compileEntityView(config: ClivlyEntitiesConfig, entityKey: string, options?: CompileOptions): CompiledView;
|
|
47
|
+
/** Compile every entity in the config into its view, in declaration order. */
|
|
48
|
+
declare function compileEntityViews(config: ClivlyEntitiesConfig, options?: CompileOptions): CompiledView[];
|
|
49
|
+
/** The outcome of dry-running one entity's compiled view against the host. */
|
|
50
|
+
interface ExplainResult {
|
|
51
|
+
entityKey: string;
|
|
52
|
+
/** The runner's error message when `ok` is false. */
|
|
53
|
+
error?: string;
|
|
54
|
+
ok: boolean;
|
|
55
|
+
/** The compiled view SQL that was run. */
|
|
56
|
+
sql: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Dry-run each entity's compiled view against the host DB via a caller-provided
|
|
60
|
+
* `run` function, so `$raw` filter/join fragments — which skip schema validation
|
|
61
|
+
* (see `validateEntitiesConfig`) — fail loudly *before* the first sync instead of
|
|
62
|
+
* at sync time. Never throws: each view's outcome is collected into an
|
|
63
|
+
* `ExplainResult`, so a mapping UI or CLI can show every problem at once.
|
|
64
|
+
*
|
|
65
|
+
* `run` should execute the SQL somewhere side-effect-free — e.g. inside a
|
|
66
|
+
* transaction it rolls back, or by `EXPLAIN`-ing the statement — since the
|
|
67
|
+
* compiled SQL is a `CREATE OR REPLACE VIEW`.
|
|
68
|
+
*/
|
|
69
|
+
declare function explainEntitiesConfig(config: ClivlyEntitiesConfig, run: (sql: string) => Promise<unknown>, options?: CompileOptions): Promise<ExplainResult[]>;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { compileEntityViews as a, compileEntityView as i, CompiledView as n, explainEntitiesConfig as o, ExplainResult as r, CompileOptions as t };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//#region ../core/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 ?? DEFAULT_VIEW_PREFIX}${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
|
|
185
|
+
Object.defineProperty(exports, "compileEntityView", {
|
|
186
|
+
enumerable: true,
|
|
187
|
+
get: function() {
|
|
188
|
+
return compileEntityView;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
Object.defineProperty(exports, "compileEntityViews", {
|
|
192
|
+
enumerable: true,
|
|
193
|
+
get: function() {
|
|
194
|
+
return compileEntityViews;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
Object.defineProperty(exports, "explainEntitiesConfig", {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
get: function() {
|
|
200
|
+
return explainEntitiesConfig;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
//#region ../core/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 ?? DEFAULT_VIEW_PREFIX}${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
|
|
185
|
+
export { compileEntityViews as n, explainEntitiesConfig as r, compileEntityView as t };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { a as ClivlyEntitiesConfig } from "./entity-config-D1DFyeUp.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/view-compiler.d.ts
|
|
4
|
+
interface CompileOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Optional tenant-scoping predicate ANDed into every view's WHERE, e.g.
|
|
7
|
+
* `{ column: "workspace_id", value: "ws_123" }`. Column must exist on the
|
|
8
|
+
* entity's source table (the compiler does not verify that — validate first).
|
|
9
|
+
*/
|
|
10
|
+
orgScope?: {
|
|
11
|
+
column: string;
|
|
12
|
+
value: string | number;
|
|
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
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Emit views `WITH (security_invoker = true)` so they run with the querying
|
|
23
|
+
* role's privileges instead of the (owner's) definer privileges — the
|
|
24
|
+
* default Postgres behaviour, which bypasses the caller's RLS. Defaults to
|
|
25
|
+
* `true`: the security boundary the product promises. Set `false` only for
|
|
26
|
+
* hosts on Postgres < 15, which lacks the option.
|
|
27
|
+
*/
|
|
28
|
+
securityInvoker?: boolean;
|
|
29
|
+
/** View name prefix. Defaults to `crm_src_`. */
|
|
30
|
+
viewPrefix?: string;
|
|
31
|
+
}
|
|
32
|
+
interface CompiledView {
|
|
33
|
+
/** Output column names in SELECT order (`id`, mapped fields, then `<rel>_ref`s). */
|
|
34
|
+
columns: string[];
|
|
35
|
+
concept: string;
|
|
36
|
+
/** `DROP VIEW IF EXISTS …` statement (for teardown / reconfiguration). */
|
|
37
|
+
dropSql: string;
|
|
38
|
+
entityKey: string;
|
|
39
|
+
/** Resolved role — the entity's `role` or `"primary"`. */
|
|
40
|
+
role: string;
|
|
41
|
+
sourceTable: string;
|
|
42
|
+
/** `CREATE OR REPLACE VIEW … AS …` statement. */
|
|
43
|
+
sql: string;
|
|
44
|
+
viewName: string;
|
|
45
|
+
}
|
|
46
|
+
declare function compileEntityView(config: ClivlyEntitiesConfig, entityKey: string, options?: CompileOptions): CompiledView;
|
|
47
|
+
/** Compile every entity in the config into its view, in declaration order. */
|
|
48
|
+
declare function compileEntityViews(config: ClivlyEntitiesConfig, options?: CompileOptions): CompiledView[];
|
|
49
|
+
/** The outcome of dry-running one entity's compiled view against the host. */
|
|
50
|
+
interface ExplainResult {
|
|
51
|
+
entityKey: string;
|
|
52
|
+
/** The runner's error message when `ok` is false. */
|
|
53
|
+
error?: string;
|
|
54
|
+
ok: boolean;
|
|
55
|
+
/** The compiled view SQL that was run. */
|
|
56
|
+
sql: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Dry-run each entity's compiled view against the host DB via a caller-provided
|
|
60
|
+
* `run` function, so `$raw` filter/join fragments — which skip schema validation
|
|
61
|
+
* (see `validateEntitiesConfig`) — fail loudly *before* the first sync instead of
|
|
62
|
+
* at sync time. Never throws: each view's outcome is collected into an
|
|
63
|
+
* `ExplainResult`, so a mapping UI or CLI can show every problem at once.
|
|
64
|
+
*
|
|
65
|
+
* `run` should execute the SQL somewhere side-effect-free — e.g. inside a
|
|
66
|
+
* transaction it rolls back, or by `EXPLAIN`-ing the statement — since the
|
|
67
|
+
* compiled SQL is a `CREATE OR REPLACE VIEW`.
|
|
68
|
+
*/
|
|
69
|
+
declare function explainEntitiesConfig(config: ClivlyEntitiesConfig, run: (sql: string) => Promise<unknown>, options?: CompileOptions): Promise<ExplainResult[]>;
|
|
70
|
+
//#endregion
|
|
71
|
+
export { compileEntityViews as a, compileEntityView as i, CompiledView as n, explainEntitiesConfig as o, ExplainResult as r, CompileOptions as t };
|
package/vite.cjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region ../sdk/src/vite.ts
|
|
3
|
+
/**
|
|
4
|
+
* Vite dev-server plugin that runs the Clivly SDK lifecycle for you: on server
|
|
5
|
+
* boot it calls `sdk.connect()` (for a clear status line) then `sdk.start()`
|
|
6
|
+
* (the heartbeat + sync keep-alive loop), and calls `sdk.stop()` when the dev
|
|
7
|
+
* server closes. Dev-only (`apply: "serve"`) — `start()`'s setInterval loop
|
|
8
|
+
* does not persist on serverless/edge builds, so production on Workers/Lambda
|
|
9
|
+
* should mount `sdk.createClivlyHandler()` and drive it from a cron trigger
|
|
10
|
+
* instead. See docs/guides/connecting-to-clivly-cloud.md.
|
|
11
|
+
*
|
|
12
|
+
* Vite is an optional peer dependency imported type-only (`import type`), so
|
|
13
|
+
* the built bundle carries no runtime dependency on Vite.
|
|
14
|
+
*/
|
|
15
|
+
function clivlyVite(sdk, options = {}) {
|
|
16
|
+
const label = options.name ?? "clivly";
|
|
17
|
+
return {
|
|
18
|
+
name: "clivly",
|
|
19
|
+
apply: "serve",
|
|
20
|
+
configureServer(server) {
|
|
21
|
+
const { logger } = server.config;
|
|
22
|
+
const boot = async () => {
|
|
23
|
+
const result = await sdk.connect();
|
|
24
|
+
if (result.ok) {
|
|
25
|
+
const org = result.org?.name ?? result.org?.id;
|
|
26
|
+
logger.info(`${label}: connected${org ? ` to ${org}` : ""}`);
|
|
27
|
+
} else logger.warn(`${label}: not connected (${result.reason})`);
|
|
28
|
+
await sdk.start();
|
|
29
|
+
};
|
|
30
|
+
boot().catch((error) => {
|
|
31
|
+
logger.error(`${label}: startup failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
});
|
|
33
|
+
server.httpServer?.once("close", () => sdk.stop());
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
exports.clivlyVite = clivlyVite;
|
package/vite.d.cts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { i as ClivlySDK } from "./namespace-probe-BtS0nXcr.cjs";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region ../sdk/src/vite.d.ts
|
|
5
|
+
interface ClivlyViteOptions {
|
|
6
|
+
/** Log prefix for Clivly's boot lines. Defaults to "clivly". */
|
|
7
|
+
name?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Vite dev-server plugin that runs the Clivly SDK lifecycle for you: on server
|
|
11
|
+
* boot it calls `sdk.connect()` (for a clear status line) then `sdk.start()`
|
|
12
|
+
* (the heartbeat + sync keep-alive loop), and calls `sdk.stop()` when the dev
|
|
13
|
+
* server closes. Dev-only (`apply: "serve"`) — `start()`'s setInterval loop
|
|
14
|
+
* does not persist on serverless/edge builds, so production on Workers/Lambda
|
|
15
|
+
* should mount `sdk.createClivlyHandler()` and drive it from a cron trigger
|
|
16
|
+
* instead. See docs/guides/connecting-to-clivly-cloud.md.
|
|
17
|
+
*
|
|
18
|
+
* Vite is an optional peer dependency imported type-only (`import type`), so
|
|
19
|
+
* the built bundle carries no runtime dependency on Vite.
|
|
20
|
+
*/
|
|
21
|
+
declare function clivlyVite(sdk: ClivlySDK, options?: ClivlyViteOptions): Plugin;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ClivlyViteOptions, clivlyVite };
|
package/vite.d.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { i as ClivlySDK } from "./namespace-probe-CYMie-BI.mjs";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region ../sdk/src/vite.d.ts
|
|
5
|
+
interface ClivlyViteOptions {
|
|
6
|
+
/** Log prefix for Clivly's boot lines. Defaults to "clivly". */
|
|
7
|
+
name?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Vite dev-server plugin that runs the Clivly SDK lifecycle for you: on server
|
|
11
|
+
* boot it calls `sdk.connect()` (for a clear status line) then `sdk.start()`
|
|
12
|
+
* (the heartbeat + sync keep-alive loop), and calls `sdk.stop()` when the dev
|
|
13
|
+
* server closes. Dev-only (`apply: "serve"`) — `start()`'s setInterval loop
|
|
14
|
+
* does not persist on serverless/edge builds, so production on Workers/Lambda
|
|
15
|
+
* should mount `sdk.createClivlyHandler()` and drive it from a cron trigger
|
|
16
|
+
* instead. See docs/guides/connecting-to-clivly-cloud.md.
|
|
17
|
+
*
|
|
18
|
+
* Vite is an optional peer dependency imported type-only (`import type`), so
|
|
19
|
+
* the built bundle carries no runtime dependency on Vite.
|
|
20
|
+
*/
|
|
21
|
+
declare function clivlyVite(sdk: ClivlySDK, options?: ClivlyViteOptions): Plugin;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ClivlyViteOptions, clivlyVite };
|
package/vite.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region ../sdk/src/vite.ts
|
|
2
|
+
/**
|
|
3
|
+
* Vite dev-server plugin that runs the Clivly SDK lifecycle for you: on server
|
|
4
|
+
* boot it calls `sdk.connect()` (for a clear status line) then `sdk.start()`
|
|
5
|
+
* (the heartbeat + sync keep-alive loop), and calls `sdk.stop()` when the dev
|
|
6
|
+
* server closes. Dev-only (`apply: "serve"`) — `start()`'s setInterval loop
|
|
7
|
+
* does not persist on serverless/edge builds, so production on Workers/Lambda
|
|
8
|
+
* should mount `sdk.createClivlyHandler()` and drive it from a cron trigger
|
|
9
|
+
* instead. See docs/guides/connecting-to-clivly-cloud.md.
|
|
10
|
+
*
|
|
11
|
+
* Vite is an optional peer dependency imported type-only (`import type`), so
|
|
12
|
+
* the built bundle carries no runtime dependency on Vite.
|
|
13
|
+
*/
|
|
14
|
+
function clivlyVite(sdk, options = {}) {
|
|
15
|
+
const label = options.name ?? "clivly";
|
|
16
|
+
return {
|
|
17
|
+
name: "clivly",
|
|
18
|
+
apply: "serve",
|
|
19
|
+
configureServer(server) {
|
|
20
|
+
const { logger } = server.config;
|
|
21
|
+
const boot = async () => {
|
|
22
|
+
const result = await sdk.connect();
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
const org = result.org?.name ?? result.org?.id;
|
|
25
|
+
logger.info(`${label}: connected${org ? ` to ${org}` : ""}`);
|
|
26
|
+
} else logger.warn(`${label}: not connected (${result.reason})`);
|
|
27
|
+
await sdk.start();
|
|
28
|
+
};
|
|
29
|
+
boot().catch((error) => {
|
|
30
|
+
logger.error(`${label}: startup failed — ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
});
|
|
32
|
+
server.httpServer?.once("close", () => sdk.stop());
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { clivlyVite };
|