@saptools/service-flow 0.1.3 → 0.1.5
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/CHANGELOG.md +18 -0
- package/README.md +11 -3
- package/TECHNICAL-NOTE.md +16 -2
- package/dist/{chunk-SIKIGT42.js → chunk-I33ASGX3.js} +322 -122
- package/dist/chunk-I33ASGX3.js.map +1 -0
- package/dist/cli.js +148 -95
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-SIKIGT42.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
parsePackageJson,
|
|
11
11
|
parseServiceBindings,
|
|
12
12
|
trace
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-I33ASGX3.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -84,27 +84,26 @@ function createWorkspaceConfig(rootPath, dbPath, ignore = [...DEFAULT_IGNORES])
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// src/db/connection.ts
|
|
87
|
-
import { execFileSync } from "child_process";
|
|
88
87
|
import fs2 from "fs";
|
|
89
88
|
import path2 from "path";
|
|
90
89
|
|
|
91
90
|
// src/db/schema.ts
|
|
92
91
|
var schemaSql = `
|
|
93
92
|
CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
94
|
-
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, UNIQUE(workspace_id, absolute_path));
|
|
95
|
-
CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path));
|
|
96
|
-
CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL);
|
|
97
|
-
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
|
|
98
|
-
CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
|
|
99
|
-
CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL);
|
|
100
|
-
CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
|
|
101
|
-
CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL);
|
|
102
|
-
CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL);
|
|
103
|
-
CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT);
|
|
104
|
-
CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT);
|
|
105
|
-
CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT);
|
|
106
|
-
CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL);
|
|
107
|
-
CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER);
|
|
93
|
+
CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
94
|
+
CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
95
|
+
CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
96
|
+
CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
|
|
97
|
+
CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE);
|
|
98
|
+
CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
|
|
99
|
+
CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
100
|
+
CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
|
|
101
|
+
CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
|
|
102
|
+
CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
|
|
104
|
+
CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
105
|
+
CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
106
|
+
CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
|
|
108
107
|
CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
|
|
109
108
|
CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
|
|
110
109
|
CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
|
|
@@ -113,87 +112,102 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, rep
|
|
|
113
112
|
`;
|
|
114
113
|
|
|
115
114
|
// src/db/migrations.ts
|
|
115
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
116
|
+
function hasColumn(db, table, column) {
|
|
117
|
+
return db.prepare(`PRAGMA table_info(${table})`).all().some((row) => row.name === column);
|
|
118
|
+
}
|
|
119
|
+
function userVersion(db) {
|
|
120
|
+
const row = db.pragma("user_version")[0];
|
|
121
|
+
return Number(row?.user_version ?? 0);
|
|
122
|
+
}
|
|
116
123
|
function migrate(db) {
|
|
117
|
-
db.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
db.prepare(
|
|
121
|
-
|
|
122
|
-
).run();
|
|
124
|
+
db.transaction(() => {
|
|
125
|
+
db.exec(schemaSql);
|
|
126
|
+
if (!hasColumn(db, "service_bindings", "helper_chain_json")) db.prepare("ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT").run();
|
|
127
|
+
if (!hasColumn(db, "service_bindings", "alias_expr")) db.prepare("ALTER TABLE service_bindings ADD COLUMN alias_expr TEXT").run();
|
|
128
|
+
if (!hasColumn(db, "repositories", "fingerprint")) db.prepare("ALTER TABLE repositories ADD COLUMN fingerprint TEXT").run();
|
|
129
|
+
if (!hasColumn(db, "graph_edges", "status")) db.prepare("ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'").run();
|
|
130
|
+
db.prepare("UPDATE graph_edges SET status=CASE WHEN edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' THEN 'resolved' WHEN edge_type IN ('HANDLER_RUNS_DB_QUERY','HANDLER_CALLS_EXTERNAL_HTTP','HANDLER_EMITS_EVENT','EVENT_CONSUMED_BY_HANDLER') THEN 'terminal' WHEN edge_type='DYNAMIC_EDGE_CANDIDATE' THEN 'dynamic' ELSE status END").run();
|
|
131
|
+
db.pragma(`user_version = ${Math.max(userVersion(db), CURRENT_SCHEMA_VERSION)}`);
|
|
132
|
+
});
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
// src/db/connection.ts
|
|
126
|
-
function
|
|
127
|
-
if (value === null || value === void 0) return "NULL";
|
|
128
|
-
if (typeof value === "number")
|
|
129
|
-
return Number.isFinite(value) ? String(value) : "NULL";
|
|
130
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
131
|
-
return `'${String(value).replaceAll("'", "''")}'`;
|
|
132
|
-
}
|
|
133
|
-
function bind(sql, params) {
|
|
134
|
-
let index = 0;
|
|
135
|
-
return sql.replaceAll("?", () => quote(params[index++]));
|
|
136
|
-
}
|
|
137
|
-
function isBusy(error) {
|
|
138
|
-
return error instanceof Error && /SQLITE_BUSY|database is locked|database is busy/i.test(error.message);
|
|
139
|
-
}
|
|
140
|
-
function sleep(ms) {
|
|
141
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
142
|
-
}
|
|
143
|
-
function call(dbPath, args, attempt = 0) {
|
|
136
|
+
function loadSqlite() {
|
|
144
137
|
try {
|
|
145
|
-
return
|
|
146
|
-
encoding: "utf8",
|
|
147
|
-
maxBuffer: 128 * 1024 * 1024,
|
|
148
|
-
env: { ...process.env, SQLITE_BUSY_TIMEOUT: "10000" }
|
|
149
|
-
});
|
|
138
|
+
return process.getBuiltinModule("node:sqlite");
|
|
150
139
|
} catch (error) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (isBusy(error))
|
|
156
|
-
throw new Error(
|
|
157
|
-
"SQLite database is busy or locked. Another service-flow writer may be active; retry after it finishes.",
|
|
158
|
-
{ cause: error }
|
|
159
|
-
);
|
|
160
|
-
throw error;
|
|
140
|
+
throw new Error(
|
|
141
|
+
"service-flow requires a persistent SQLite driver. This build uses node:sqlite; run on Node.js with node:sqlite support or install a package build with its native SQLite dependency.",
|
|
142
|
+
{ cause: error }
|
|
143
|
+
);
|
|
161
144
|
}
|
|
162
145
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
146
|
+
function bindParams(params) {
|
|
147
|
+
return params.map((param) => {
|
|
148
|
+
if (param === void 0 || param === null) return null;
|
|
149
|
+
if (typeof param === "string" || typeof param === "number" || typeof param === "bigint" || Buffer.isBuffer(param)) return param;
|
|
150
|
+
if (typeof param === "boolean") return param ? 1 : 0;
|
|
151
|
+
return JSON.stringify(param);
|
|
152
|
+
});
|
|
167
153
|
}
|
|
168
|
-
function openDatabase(dbPath) {
|
|
154
|
+
function openDatabase(dbPath, options = {}) {
|
|
169
155
|
fs2.mkdirSync(path2.dirname(dbPath), { recursive: true });
|
|
156
|
+
const sqlite = loadSqlite();
|
|
157
|
+
const native = new sqlite.DatabaseSync(dbPath, { readOnly: Boolean(options.readonly) });
|
|
158
|
+
let inTransaction = false;
|
|
170
159
|
const db = {
|
|
171
160
|
path: dbPath,
|
|
161
|
+
readonly: Boolean(options.readonly),
|
|
172
162
|
exec(sql) {
|
|
173
|
-
|
|
163
|
+
native.exec(sql);
|
|
174
164
|
},
|
|
175
165
|
prepare(sql) {
|
|
166
|
+
const statement = native.prepare(sql);
|
|
176
167
|
return {
|
|
177
|
-
run: (...params) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
},
|
|
181
|
-
get: (...params) => jsonRows(dbPath, bind(sql, params))[0],
|
|
182
|
-
all: (...params) => jsonRows(dbPath, bind(sql, params))
|
|
168
|
+
run: (...params) => statement.run(...bindParams(params)),
|
|
169
|
+
get: (...params) => statement.get(...bindParams(params)),
|
|
170
|
+
all: (...params) => statement.all(...bindParams(params))
|
|
183
171
|
};
|
|
184
172
|
},
|
|
185
173
|
pragma(sql) {
|
|
186
|
-
|
|
174
|
+
const normalized = sql.trim().replace(/;$/, "");
|
|
175
|
+
if (/=/.test(normalized)) {
|
|
176
|
+
native.exec(`PRAGMA ${normalized}`);
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
return native.prepare(`PRAGMA ${normalized}`).all();
|
|
180
|
+
},
|
|
181
|
+
transaction(fn) {
|
|
182
|
+
if (inTransaction) return fn();
|
|
183
|
+
inTransaction = true;
|
|
184
|
+
native.exec("BEGIN IMMEDIATE");
|
|
185
|
+
try {
|
|
186
|
+
const result = fn();
|
|
187
|
+
native.exec("COMMIT");
|
|
188
|
+
return result;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
native.exec("ROLLBACK");
|
|
191
|
+
throw error;
|
|
192
|
+
} finally {
|
|
193
|
+
inTransaction = false;
|
|
194
|
+
}
|
|
187
195
|
},
|
|
188
196
|
close() {
|
|
197
|
+
native.close();
|
|
189
198
|
}
|
|
190
199
|
};
|
|
191
200
|
db.pragma("busy_timeout = 10000");
|
|
192
|
-
db.pragma("journal_mode = WAL");
|
|
193
201
|
db.pragma("foreign_keys = ON");
|
|
194
|
-
|
|
202
|
+
if (!options.readonly) {
|
|
203
|
+
db.pragma("journal_mode = WAL");
|
|
204
|
+
if (options.migrate !== false) migrate(db);
|
|
205
|
+
}
|
|
195
206
|
return db;
|
|
196
207
|
}
|
|
208
|
+
function openReadOnlyDatabase(dbPath) {
|
|
209
|
+
return openDatabase(dbPath, { readonly: true, migrate: false });
|
|
210
|
+
}
|
|
197
211
|
|
|
198
212
|
// src/db/repositories.ts
|
|
199
213
|
function upsertWorkspace(db, rootPath, dbPath) {
|
|
@@ -354,13 +368,14 @@ function insertRegistrations(db, repoId, rows) {
|
|
|
354
368
|
}
|
|
355
369
|
function insertBindings(db, repoId, rows) {
|
|
356
370
|
const stmt = db.prepare(
|
|
357
|
-
"INSERT INTO service_bindings(repo_id,variable_name,alias,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,helper_chain_json) VALUES(
|
|
371
|
+
"INSERT INTO service_bindings(repo_id,variable_name,alias,alias_expr,destination_expr,service_path_expr,is_dynamic,placeholders_json,source_file,source_line,helper_chain_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
|
358
372
|
);
|
|
359
373
|
for (const r of rows)
|
|
360
374
|
stmt.run(
|
|
361
375
|
repoId,
|
|
362
376
|
r.variableName,
|
|
363
377
|
r.alias,
|
|
378
|
+
r.aliasExpr,
|
|
364
379
|
r.destinationExpr,
|
|
365
380
|
r.servicePathExpr,
|
|
366
381
|
r.isDynamic ? 1 : 0,
|
|
@@ -446,6 +461,9 @@ import { readFile } from "fs/promises";
|
|
|
446
461
|
async function sha256File(filePath) {
|
|
447
462
|
return createHash("sha256").update(await readFile(filePath)).digest("hex");
|
|
448
463
|
}
|
|
464
|
+
function sha256Text(text) {
|
|
465
|
+
return createHash("sha256").update(text).digest("hex");
|
|
466
|
+
}
|
|
449
467
|
|
|
450
468
|
// src/indexer/incremental-index.ts
|
|
451
469
|
async function recordFile(db, repoId, repoPath, relativeFile) {
|
|
@@ -471,10 +489,12 @@ function errorMessage(error) {
|
|
|
471
489
|
|
|
472
490
|
// src/indexer/repository-indexer.ts
|
|
473
491
|
async function indexRepository(db, repo, force) {
|
|
474
|
-
void force;
|
|
475
492
|
let diagnostics = 0;
|
|
476
493
|
let files = 0;
|
|
494
|
+
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
477
495
|
const facts = await parsePackageJson(repo.absolute_path);
|
|
496
|
+
const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, facts.dependencies, Boolean(force));
|
|
497
|
+
if (!force && repo.fingerprint === fingerprint) return { fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
478
498
|
const kind = await classifyRepository(repo.absolute_path, facts);
|
|
479
499
|
db.prepare(
|
|
480
500
|
"UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?"
|
|
@@ -488,7 +508,6 @@ async function indexRepository(db, repo, force) {
|
|
|
488
508
|
);
|
|
489
509
|
clearRepoFacts(db, repo.id);
|
|
490
510
|
insertRequires(db, repo.id, facts.cdsRequires);
|
|
491
|
-
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
492
511
|
for (const file of sourceFiles) {
|
|
493
512
|
try {
|
|
494
513
|
await recordFile(db, repo.id, repo.absolute_path, file);
|
|
@@ -523,14 +542,15 @@ async function indexRepository(db, repo, force) {
|
|
|
523
542
|
}
|
|
524
543
|
}
|
|
525
544
|
db.prepare(
|
|
526
|
-
"UPDATE repositories SET last_indexed_at=?, index_status=?, error_count=? WHERE id=?"
|
|
545
|
+
"UPDATE repositories SET last_indexed_at=?, index_status=?, error_count=?, fingerprint=? WHERE id=?"
|
|
527
546
|
).run(
|
|
528
547
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
529
548
|
diagnostics ? "partial" : "indexed",
|
|
530
549
|
diagnostics,
|
|
550
|
+
fingerprint,
|
|
531
551
|
repo.id
|
|
532
552
|
);
|
|
533
|
-
return { fileCount: files, diagnosticCount: diagnostics };
|
|
553
|
+
return { fileCount: files, diagnosticCount: diagnostics, skipped: false };
|
|
534
554
|
}
|
|
535
555
|
async function findSourceFiles(root) {
|
|
536
556
|
const out = [];
|
|
@@ -541,12 +561,28 @@ async function findSourceFiles(root) {
|
|
|
541
561
|
if (e.isDirectory()) {
|
|
542
562
|
if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name))
|
|
543
563
|
await walk(path5.join(dir, e.name), rel);
|
|
544
|
-
} else if (/\.(cds|ts|js)$/.test(e.name)
|
|
564
|
+
} else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel))
|
|
565
|
+
out.push(rel);
|
|
545
566
|
}
|
|
546
567
|
}
|
|
547
568
|
await walk(root);
|
|
548
569
|
return out.sort();
|
|
549
570
|
}
|
|
571
|
+
function isDefaultTestFile(relativeFile) {
|
|
572
|
+
const parts = relativeFile.split("/");
|
|
573
|
+
if (parts.some((part) => ["test", "tests", "__tests__"].includes(part)))
|
|
574
|
+
return true;
|
|
575
|
+
return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
|
|
576
|
+
}
|
|
577
|
+
async function repositoryFingerprint(root, files, dependencies, force) {
|
|
578
|
+
void force;
|
|
579
|
+
const entries = ["schema:2", `deps:${JSON.stringify(Object.entries(dependencies).sort())}`];
|
|
580
|
+
for (const file of files) {
|
|
581
|
+
const content = await fs5.readFile(path5.join(root, file), "utf8").catch(() => "");
|
|
582
|
+
entries.push(`${file}:${sha256Text(content)}`);
|
|
583
|
+
}
|
|
584
|
+
return sha256Text(entries.join("\n"));
|
|
585
|
+
}
|
|
550
586
|
|
|
551
587
|
// src/indexer/workspace-indexer.ts
|
|
552
588
|
async function indexWorkspace(db, workspaceId, options) {
|
|
@@ -559,10 +595,12 @@ async function indexWorkspace(db, workspaceId, options) {
|
|
|
559
595
|
);
|
|
560
596
|
let fileCount = 0;
|
|
561
597
|
let diagnosticCount = 0;
|
|
598
|
+
let skippedCount = 0;
|
|
562
599
|
for (const repo of repos) {
|
|
563
600
|
const result = await indexRepository(db, repo, options.force);
|
|
564
601
|
fileCount += result.fileCount;
|
|
565
602
|
diagnosticCount += result.diagnosticCount;
|
|
603
|
+
skippedCount += result.skipped ? 1 : 0;
|
|
566
604
|
}
|
|
567
605
|
db.prepare(
|
|
568
606
|
"UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?"
|
|
@@ -573,7 +611,7 @@ async function indexWorkspace(db, workspaceId, options) {
|
|
|
573
611
|
diagnosticCount,
|
|
574
612
|
runId
|
|
575
613
|
);
|
|
576
|
-
return { repoCount: repos.length, fileCount, diagnosticCount };
|
|
614
|
+
return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
|
|
577
615
|
}
|
|
578
616
|
|
|
579
617
|
// src/trace/selectors.ts
|
|
@@ -674,11 +712,22 @@ async function withWorkspace(workspace, fn) {
|
|
|
674
712
|
db.close();
|
|
675
713
|
}
|
|
676
714
|
}
|
|
715
|
+
async function withReadOnlyWorkspace(workspace, fn) {
|
|
716
|
+
const config = await loadWorkspaceConfig(workspace);
|
|
717
|
+
const db = openReadOnlyDatabase(config.dbPath);
|
|
718
|
+
try {
|
|
719
|
+
const row = getWorkspace(db, config.rootPath);
|
|
720
|
+
if (!row) throw new Error(`Workspace is not initialized in ${config.dbPath}`);
|
|
721
|
+
return await fn(db, row.id, config.rootPath);
|
|
722
|
+
} finally {
|
|
723
|
+
db.close();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
677
726
|
function createProgram() {
|
|
678
727
|
const program = new Command();
|
|
679
728
|
program.name("service-flow").description(
|
|
680
729
|
"Trace SAP CAP service-to-service flows across multi-repository workspaces"
|
|
681
|
-
).version("0.1.
|
|
730
|
+
).version("0.1.5");
|
|
682
731
|
program.command("init").argument("<workspace>").option("--db <path>").option("--ignore <pattern...>").action(
|
|
683
732
|
(workspace, opts) => void init(workspace, opts).catch(fail)
|
|
684
733
|
);
|
|
@@ -689,7 +738,7 @@ function createProgram() {
|
|
|
689
738
|
force: Boolean(opts.force)
|
|
690
739
|
});
|
|
691
740
|
process.stdout.write(
|
|
692
|
-
`Indexed ${r.
|
|
741
|
+
`Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics
|
|
693
742
|
`
|
|
694
743
|
);
|
|
695
744
|
}).catch(fail)
|
|
@@ -698,13 +747,13 @@ function createProgram() {
|
|
|
698
747
|
(opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
699
748
|
const r = linkWorkspace(db, workspaceId);
|
|
700
749
|
process.stdout.write(
|
|
701
|
-
`Linked ${r.edgeCount} edges, ${r.unresolvedCount} unresolved
|
|
750
|
+
`Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal
|
|
702
751
|
`
|
|
703
752
|
);
|
|
704
753
|
}).catch(fail)
|
|
705
754
|
);
|
|
706
755
|
program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").option("--format <format>", "table|json|mermaid", "table").option("--include-external").option("--include-db").option("--include-async").option("--var <key=value>", "dynamic variable", collect, []).action(
|
|
707
|
-
(opts) => void
|
|
756
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
708
757
|
const result = trace(
|
|
709
758
|
db,
|
|
710
759
|
{
|
|
@@ -729,7 +778,7 @@ function createProgram() {
|
|
|
729
778
|
);
|
|
730
779
|
const list = program.command("list");
|
|
731
780
|
list.command("repos").option("--workspace <path>").action(
|
|
732
|
-
(opts) => void
|
|
781
|
+
(opts) => void withReadOnlyWorkspace(
|
|
733
782
|
opts.workspace,
|
|
734
783
|
(db) => process.stdout.write(
|
|
735
784
|
renderJson(
|
|
@@ -743,7 +792,7 @@ function createProgram() {
|
|
|
743
792
|
).catch(fail)
|
|
744
793
|
);
|
|
745
794
|
list.command("services").option("--workspace <path>").option("--repo <name>").action(
|
|
746
|
-
(opts) => void
|
|
795
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
747
796
|
const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
|
|
748
797
|
const rows = db.prepare(
|
|
749
798
|
"SELECT r.name repo,s.service_path servicePath,s.qualified_name qualifiedName FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path"
|
|
@@ -752,7 +801,7 @@ function createProgram() {
|
|
|
752
801
|
}).catch(fail)
|
|
753
802
|
);
|
|
754
803
|
list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
|
|
755
|
-
(opts) => void
|
|
804
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
756
805
|
const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
|
|
757
806
|
const rows = db.prepare(
|
|
758
807
|
"SELECT r.name repo,s.service_path servicePath,o.operation_name operation,o.operation_path path FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)"
|
|
@@ -761,7 +810,7 @@ function createProgram() {
|
|
|
761
810
|
}).catch(fail)
|
|
762
811
|
);
|
|
763
812
|
list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
|
|
764
|
-
(opts) => void
|
|
813
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
765
814
|
const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
|
|
766
815
|
const rows = db.prepare(
|
|
767
816
|
"SELECT r.name repo,c.call_type type,c.operation_path_expr path,c.source_file file,c.source_line line FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)"
|
|
@@ -776,8 +825,8 @@ function createProgram() {
|
|
|
776
825
|
process.stdout.write(renderJson(rows));
|
|
777
826
|
}).catch(fail)
|
|
778
827
|
);
|
|
779
|
-
program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").action(
|
|
780
|
-
(opts) => void
|
|
828
|
+
program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").option("--var <key=value>", "dynamic variable", collect, []).action(
|
|
829
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
781
830
|
const result = trace(
|
|
782
831
|
db,
|
|
783
832
|
{
|
|
@@ -790,7 +839,8 @@ function createProgram() {
|
|
|
790
839
|
depth: 100,
|
|
791
840
|
includeAsync: true,
|
|
792
841
|
includeDb: true,
|
|
793
|
-
includeExternal: true
|
|
842
|
+
includeExternal: true,
|
|
843
|
+
vars: parseVars(opts.var)
|
|
794
844
|
}
|
|
795
845
|
);
|
|
796
846
|
process.stdout.write(
|
|
@@ -800,7 +850,7 @@ function createProgram() {
|
|
|
800
850
|
);
|
|
801
851
|
const inspect = program.command("inspect");
|
|
802
852
|
inspect.command("repo").argument("<name>").option("--workspace <path>").action(
|
|
803
|
-
(name, opts) => void
|
|
853
|
+
(name, opts) => void withReadOnlyWorkspace(
|
|
804
854
|
opts.workspace,
|
|
805
855
|
(db) => process.stdout.write(
|
|
806
856
|
renderJson(repoByName(db, name) ?? { error: "repo not found" })
|
|
@@ -808,7 +858,7 @@ function createProgram() {
|
|
|
808
858
|
).catch(fail)
|
|
809
859
|
);
|
|
810
860
|
inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
|
|
811
|
-
(selector, opts) => void
|
|
861
|
+
(selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
812
862
|
const rows = db.prepare(
|
|
813
863
|
"SELECT * FROM cds_operations WHERE operation_name=? OR operation_path=?"
|
|
814
864
|
).all(selector, selector);
|
|
@@ -816,24 +866,27 @@ function createProgram() {
|
|
|
816
866
|
}).catch(fail)
|
|
817
867
|
);
|
|
818
868
|
program.command("doctor").option("--workspace <path>").option("--strict").action(
|
|
819
|
-
(opts) => void
|
|
869
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
820
870
|
const diagnostics = db.prepare(
|
|
821
871
|
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id"
|
|
822
|
-
).all(
|
|
872
|
+
).all();
|
|
823
873
|
const health = db.prepare(
|
|
824
874
|
`SELECT 'info' severity,'entity_only_service' code,'CDS service has no action/function/event operations; this can be valid for entity-only services' message,s.source_file sourceFile,s.source_line sourceLine
|
|
825
875
|
FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
|
|
826
876
|
UNION ALL
|
|
827
877
|
SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
|
|
828
|
-
FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND s.is_extend=1
|
|
878
|
+
FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND s.is_extend=1 AND ?
|
|
829
879
|
UNION ALL
|
|
830
880
|
SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
|
|
831
881
|
FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
|
|
832
882
|
WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
|
|
833
883
|
UNION ALL
|
|
834
884
|
SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
|
|
835
|
-
WHERE NOT EXISTS (SELECT 1 FROM search_index)
|
|
836
|
-
|
|
885
|
+
WHERE NOT EXISTS (SELECT 1 FROM search_index)
|
|
886
|
+
UNION ALL
|
|
887
|
+
SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
|
|
888
|
+
WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)`
|
|
889
|
+
).all(Boolean(opts.strict), Boolean(opts.strict));
|
|
837
890
|
const allDiagnostics = [...diagnostics, ...health];
|
|
838
891
|
process.stdout.write(
|
|
839
892
|
allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
|