@saptools/service-flow 0.1.4 → 0.1.6
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 +14 -3
- package/TECHNICAL-NOTE.md +19 -2
- package/dist/{chunk-JY6GBGZT.js → chunk-6C5HZ6IR.js} +236 -144
- package/dist/chunk-6C5HZ6IR.js.map +1 -0
- package/dist/cli.js +234 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-JY6GBGZT.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-6C5HZ6IR.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -84,14 +84,13 @@ 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), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
|
|
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);
|
|
95
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);
|
|
96
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);
|
|
97
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);
|
|
@@ -102,9 +101,9 @@ CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_clas
|
|
|
102
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);
|
|
103
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);
|
|
104
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);
|
|
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);
|
|
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,89 +112,108 @@ 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();
|
|
123
|
-
|
|
124
|
-
db.
|
|
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
|
+
});
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
// src/db/connection.ts
|
|
128
|
-
function
|
|
129
|
-
if (value === null || value === void 0) return "NULL";
|
|
130
|
-
if (typeof value === "number")
|
|
131
|
-
return Number.isFinite(value) ? String(value) : "NULL";
|
|
132
|
-
if (typeof value === "boolean") return value ? "1" : "0";
|
|
133
|
-
return `'${String(value).replaceAll("'", "''")}'`;
|
|
134
|
-
}
|
|
135
|
-
function bind(sql, params) {
|
|
136
|
-
let index = 0;
|
|
137
|
-
return sql.replaceAll("?", () => quote(params[index++]));
|
|
138
|
-
}
|
|
139
|
-
function isBusy(error) {
|
|
140
|
-
return error instanceof Error && /SQLITE_BUSY|database is locked|database is busy/i.test(error.message);
|
|
141
|
-
}
|
|
142
|
-
function sleep(ms) {
|
|
143
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
144
|
-
}
|
|
145
|
-
function call(dbPath, args, attempt = 0) {
|
|
136
|
+
function loadSqlite() {
|
|
146
137
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
138
|
+
const moduleValue = process.getBuiltinModule("node:sqlite");
|
|
139
|
+
if (!moduleValue || typeof moduleValue !== "object" || !("DatabaseSync" in moduleValue))
|
|
140
|
+
throw new Error("node:sqlite DatabaseSync is unavailable");
|
|
141
|
+
const sqlite = moduleValue;
|
|
142
|
+
if (typeof sqlite.DatabaseSync !== "function")
|
|
143
|
+
throw new Error("node:sqlite DatabaseSync is not a constructor");
|
|
144
|
+
return sqlite;
|
|
152
145
|
} catch (error) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (isBusy(error))
|
|
158
|
-
throw new Error(
|
|
159
|
-
"SQLite database is busy or locked. Another service-flow writer may be active; retry after it finishes.",
|
|
160
|
-
{ cause: error }
|
|
161
|
-
);
|
|
162
|
-
throw error;
|
|
146
|
+
throw new Error(
|
|
147
|
+
"service-flow 0.1.6 requires Node.js >=24 with node:sqlite DatabaseSync support. Upgrade Node.js or install a service-flow build with a compatible SQLite driver.",
|
|
148
|
+
{ cause: error }
|
|
149
|
+
);
|
|
163
150
|
}
|
|
164
151
|
}
|
|
165
|
-
function
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
152
|
+
function bindParams(params) {
|
|
153
|
+
return params.map((param) => {
|
|
154
|
+
if (param === void 0 || param === null) return null;
|
|
155
|
+
if (typeof param === "string" || typeof param === "number" || typeof param === "bigint" || Buffer.isBuffer(param)) return param;
|
|
156
|
+
if (typeof param === "boolean") return param ? 1 : 0;
|
|
157
|
+
return JSON.stringify(param);
|
|
158
|
+
});
|
|
169
159
|
}
|
|
170
|
-
function openDatabase(dbPath) {
|
|
160
|
+
function openDatabase(dbPath, options = {}) {
|
|
171
161
|
fs2.mkdirSync(path2.dirname(dbPath), { recursive: true });
|
|
162
|
+
const sqlite = loadSqlite();
|
|
163
|
+
const native = new sqlite.DatabaseSync(dbPath, { readOnly: Boolean(options.readonly) });
|
|
164
|
+
let inTransaction = false;
|
|
172
165
|
const db = {
|
|
173
166
|
path: dbPath,
|
|
167
|
+
readonly: Boolean(options.readonly),
|
|
174
168
|
exec(sql) {
|
|
175
|
-
|
|
169
|
+
native.exec(sql);
|
|
176
170
|
},
|
|
177
171
|
prepare(sql) {
|
|
172
|
+
const statement = native.prepare(sql);
|
|
178
173
|
return {
|
|
179
|
-
run: (...params) =>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
},
|
|
183
|
-
get: (...params) => jsonRows(dbPath, bind(sql, params))[0],
|
|
184
|
-
all: (...params) => jsonRows(dbPath, bind(sql, params))
|
|
174
|
+
run: (...params) => statement.run(...bindParams(params)),
|
|
175
|
+
get: (...params) => statement.get(...bindParams(params)),
|
|
176
|
+
all: (...params) => statement.all(...bindParams(params))
|
|
185
177
|
};
|
|
186
178
|
},
|
|
187
179
|
pragma(sql) {
|
|
188
|
-
|
|
180
|
+
const normalized = sql.trim().replace(/;$/, "");
|
|
181
|
+
if (/=/.test(normalized)) {
|
|
182
|
+
native.exec(`PRAGMA ${normalized}`);
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
return native.prepare(`PRAGMA ${normalized}`).all();
|
|
186
|
+
},
|
|
187
|
+
transaction(fn) {
|
|
188
|
+
if (inTransaction) return fn();
|
|
189
|
+
inTransaction = true;
|
|
190
|
+
native.exec("BEGIN IMMEDIATE");
|
|
191
|
+
try {
|
|
192
|
+
const result = fn();
|
|
193
|
+
native.exec("COMMIT");
|
|
194
|
+
return result;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
native.exec("ROLLBACK");
|
|
197
|
+
throw error;
|
|
198
|
+
} finally {
|
|
199
|
+
inTransaction = false;
|
|
200
|
+
}
|
|
189
201
|
},
|
|
190
202
|
close() {
|
|
203
|
+
native.close();
|
|
191
204
|
}
|
|
192
205
|
};
|
|
193
206
|
db.pragma("busy_timeout = 10000");
|
|
194
|
-
db.pragma("journal_mode = WAL");
|
|
195
207
|
db.pragma("foreign_keys = ON");
|
|
196
|
-
|
|
208
|
+
if (!options.readonly) {
|
|
209
|
+
db.pragma("journal_mode = WAL");
|
|
210
|
+
if (options.migrate !== false) migrate(db);
|
|
211
|
+
}
|
|
197
212
|
return db;
|
|
198
213
|
}
|
|
214
|
+
function openReadOnlyDatabase(dbPath) {
|
|
215
|
+
return openDatabase(dbPath, { readonly: true, migrate: false });
|
|
216
|
+
}
|
|
199
217
|
|
|
200
218
|
// src/db/repositories.ts
|
|
201
219
|
function upsertWorkspace(db, rootPath, dbPath) {
|
|
@@ -449,6 +467,9 @@ import { readFile } from "fs/promises";
|
|
|
449
467
|
async function sha256File(filePath) {
|
|
450
468
|
return createHash("sha256").update(await readFile(filePath)).digest("hex");
|
|
451
469
|
}
|
|
470
|
+
function sha256Text(text) {
|
|
471
|
+
return createHash("sha256").update(text).digest("hex");
|
|
472
|
+
}
|
|
452
473
|
|
|
453
474
|
// src/indexer/incremental-index.ts
|
|
454
475
|
async function recordFile(db, repoId, repoPath, relativeFile) {
|
|
@@ -472,12 +493,91 @@ function errorMessage(error) {
|
|
|
472
493
|
return error instanceof Error ? error.message : String(error);
|
|
473
494
|
}
|
|
474
495
|
|
|
496
|
+
// package.json
|
|
497
|
+
var package_default = {
|
|
498
|
+
name: "@saptools/service-flow",
|
|
499
|
+
version: "0.1.6",
|
|
500
|
+
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
501
|
+
type: "module",
|
|
502
|
+
publishConfig: {
|
|
503
|
+
access: "public",
|
|
504
|
+
registry: "https://registry.npmjs.org/"
|
|
505
|
+
},
|
|
506
|
+
bin: {
|
|
507
|
+
"service-flow": "dist/cli.js"
|
|
508
|
+
},
|
|
509
|
+
main: "./dist/index.js",
|
|
510
|
+
types: "./dist/index.d.ts",
|
|
511
|
+
exports: {
|
|
512
|
+
".": {
|
|
513
|
+
types: "./dist/index.d.ts",
|
|
514
|
+
import: "./dist/index.js"
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
files: [
|
|
518
|
+
"CHANGELOG.md",
|
|
519
|
+
"README.md",
|
|
520
|
+
"TECHNICAL-NOTE.md",
|
|
521
|
+
"dist"
|
|
522
|
+
],
|
|
523
|
+
engines: {
|
|
524
|
+
node: ">=24.0.0"
|
|
525
|
+
},
|
|
526
|
+
scripts: {
|
|
527
|
+
build: "tsup",
|
|
528
|
+
typecheck: "tsc --noEmit",
|
|
529
|
+
lint: "eslint src tests",
|
|
530
|
+
"test:unit": "vitest run tests/unit",
|
|
531
|
+
"test:e2e": "vitest run tests/e2e",
|
|
532
|
+
"test:e2e:fake": "vitest run tests/e2e"
|
|
533
|
+
},
|
|
534
|
+
keywords: [
|
|
535
|
+
"sap",
|
|
536
|
+
"cap",
|
|
537
|
+
"cds",
|
|
538
|
+
"btp",
|
|
539
|
+
"cli",
|
|
540
|
+
"service-graph",
|
|
541
|
+
"call-graph",
|
|
542
|
+
"sqlite",
|
|
543
|
+
"saptools"
|
|
544
|
+
],
|
|
545
|
+
author: "Dong Tran",
|
|
546
|
+
license: "MIT",
|
|
547
|
+
repository: {
|
|
548
|
+
type: "git",
|
|
549
|
+
url: "git+https://github.com/dongitran/saptools.git",
|
|
550
|
+
directory: "packages/service-flow"
|
|
551
|
+
},
|
|
552
|
+
homepage: "https://github.com/dongitran/saptools/tree/main/packages/service-flow#readme",
|
|
553
|
+
bugs: {
|
|
554
|
+
url: "https://github.com/dongitran/saptools/issues"
|
|
555
|
+
},
|
|
556
|
+
dependencies: {
|
|
557
|
+
commander: "13.1.0",
|
|
558
|
+
picocolors: "1.1.1",
|
|
559
|
+
typescript: "5.9.3",
|
|
560
|
+
zod: "4.4.3"
|
|
561
|
+
},
|
|
562
|
+
devDependencies: {
|
|
563
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
564
|
+
tsup: "8.5.1",
|
|
565
|
+
vitest: "3.2.4"
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// src/version.ts
|
|
570
|
+
var VERSION = package_default.version;
|
|
571
|
+
var ANALYZER_VERSION = package_default.version;
|
|
572
|
+
|
|
475
573
|
// src/indexer/repository-indexer.ts
|
|
476
574
|
async function indexRepository(db, repo, force) {
|
|
477
|
-
void force;
|
|
478
575
|
let diagnostics = 0;
|
|
479
576
|
let files = 0;
|
|
577
|
+
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
480
578
|
const facts = await parsePackageJson(repo.absolute_path);
|
|
579
|
+
const fingerprint = await repositoryFingerprint(repo.absolute_path, sourceFiles, facts, Boolean(force));
|
|
580
|
+
if (!force && repo.fingerprint === fingerprint) return { fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
481
581
|
const kind = await classifyRepository(repo.absolute_path, facts);
|
|
482
582
|
db.prepare(
|
|
483
583
|
"UPDATE repositories SET package_name=?, package_version=?, dependencies_json=?, kind=?, index_status=? WHERE id=?"
|
|
@@ -491,7 +591,6 @@ async function indexRepository(db, repo, force) {
|
|
|
491
591
|
);
|
|
492
592
|
clearRepoFacts(db, repo.id);
|
|
493
593
|
insertRequires(db, repo.id, facts.cdsRequires);
|
|
494
|
-
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
495
594
|
for (const file of sourceFiles) {
|
|
496
595
|
try {
|
|
497
596
|
await recordFile(db, repo.id, repo.absolute_path, file);
|
|
@@ -526,14 +625,15 @@ async function indexRepository(db, repo, force) {
|
|
|
526
625
|
}
|
|
527
626
|
}
|
|
528
627
|
db.prepare(
|
|
529
|
-
"UPDATE repositories SET last_indexed_at=?, index_status=?, error_count=? WHERE id=?"
|
|
628
|
+
"UPDATE repositories SET last_indexed_at=?, index_status=?, error_count=?, fingerprint=? WHERE id=?"
|
|
530
629
|
).run(
|
|
531
630
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
532
631
|
diagnostics ? "partial" : "indexed",
|
|
533
632
|
diagnostics,
|
|
633
|
+
fingerprint,
|
|
534
634
|
repo.id
|
|
535
635
|
);
|
|
536
|
-
return { fileCount: files, diagnosticCount: diagnostics };
|
|
636
|
+
return { fileCount: files, diagnosticCount: diagnostics, skipped: false };
|
|
537
637
|
}
|
|
538
638
|
async function findSourceFiles(root) {
|
|
539
639
|
const out = [];
|
|
@@ -557,6 +657,26 @@ function isDefaultTestFile(relativeFile) {
|
|
|
557
657
|
return true;
|
|
558
658
|
return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
|
|
559
659
|
}
|
|
660
|
+
async function repositoryFingerprint(root, files, facts, force) {
|
|
661
|
+
void force;
|
|
662
|
+
const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
|
|
663
|
+
const normalizedFacts = {
|
|
664
|
+
analyzerVersion: ANALYZER_VERSION,
|
|
665
|
+
packageName: facts.packageName,
|
|
666
|
+
packageVersion: facts.packageVersion,
|
|
667
|
+
dependencies: Object.fromEntries(Object.entries(facts.dependencies).sort()),
|
|
668
|
+
cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
669
|
+
scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
|
|
670
|
+
includeTests: false,
|
|
671
|
+
packageJsonHash: sha256Text(packageJson)
|
|
672
|
+
};
|
|
673
|
+
const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
674
|
+
for (const file of files) {
|
|
675
|
+
const content = await fs5.readFile(path5.join(root, file), "utf8").catch(() => "");
|
|
676
|
+
entries.push(`${file}:${sha256Text(content)}`);
|
|
677
|
+
}
|
|
678
|
+
return sha256Text(entries.join("\n"));
|
|
679
|
+
}
|
|
560
680
|
|
|
561
681
|
// src/indexer/workspace-indexer.ts
|
|
562
682
|
async function indexWorkspace(db, workspaceId, options) {
|
|
@@ -569,10 +689,12 @@ async function indexWorkspace(db, workspaceId, options) {
|
|
|
569
689
|
);
|
|
570
690
|
let fileCount = 0;
|
|
571
691
|
let diagnosticCount = 0;
|
|
692
|
+
let skippedCount = 0;
|
|
572
693
|
for (const repo of repos) {
|
|
573
694
|
const result = await indexRepository(db, repo, options.force);
|
|
574
695
|
fileCount += result.fileCount;
|
|
575
696
|
diagnosticCount += result.diagnosticCount;
|
|
697
|
+
skippedCount += result.skipped ? 1 : 0;
|
|
576
698
|
}
|
|
577
699
|
db.prepare(
|
|
578
700
|
"UPDATE index_runs SET finished_at=?, status=?, file_count=?, diagnostic_count=? WHERE id=?"
|
|
@@ -583,7 +705,7 @@ async function indexWorkspace(db, workspaceId, options) {
|
|
|
583
705
|
diagnosticCount,
|
|
584
706
|
runId
|
|
585
707
|
);
|
|
586
|
-
return { repoCount: repos.length, fileCount, diagnosticCount };
|
|
708
|
+
return { repoCount: repos.length, indexedCount: repos.length - skippedCount, skippedCount, fileCount, diagnosticCount };
|
|
587
709
|
}
|
|
588
710
|
|
|
589
711
|
// src/trace/selectors.ts
|
|
@@ -684,11 +806,22 @@ async function withWorkspace(workspace, fn) {
|
|
|
684
806
|
db.close();
|
|
685
807
|
}
|
|
686
808
|
}
|
|
809
|
+
async function withReadOnlyWorkspace(workspace, fn) {
|
|
810
|
+
const config = await loadWorkspaceConfig(workspace);
|
|
811
|
+
const db = openReadOnlyDatabase(config.dbPath);
|
|
812
|
+
try {
|
|
813
|
+
const row = getWorkspace(db, config.rootPath);
|
|
814
|
+
if (!row) throw new Error(`Workspace is not initialized in ${config.dbPath}`);
|
|
815
|
+
return await fn(db, row.id, config.rootPath);
|
|
816
|
+
} finally {
|
|
817
|
+
db.close();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
687
820
|
function createProgram() {
|
|
688
821
|
const program = new Command();
|
|
689
822
|
program.name("service-flow").description(
|
|
690
823
|
"Trace SAP CAP service-to-service flows across multi-repository workspaces"
|
|
691
|
-
).version(
|
|
824
|
+
).version(VERSION);
|
|
692
825
|
program.command("init").argument("<workspace>").option("--db <path>").option("--ignore <pattern...>").action(
|
|
693
826
|
(workspace, opts) => void init(workspace, opts).catch(fail)
|
|
694
827
|
);
|
|
@@ -699,7 +832,7 @@ function createProgram() {
|
|
|
699
832
|
force: Boolean(opts.force)
|
|
700
833
|
});
|
|
701
834
|
process.stdout.write(
|
|
702
|
-
`Indexed ${r.
|
|
835
|
+
`Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics
|
|
703
836
|
`
|
|
704
837
|
);
|
|
705
838
|
}).catch(fail)
|
|
@@ -708,13 +841,13 @@ function createProgram() {
|
|
|
708
841
|
(opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
709
842
|
const r = linkWorkspace(db, workspaceId);
|
|
710
843
|
process.stdout.write(
|
|
711
|
-
`Linked ${r.edgeCount} edges, ${r.unresolvedCount} unresolved
|
|
844
|
+
`Linked ${r.edgeCount} edges: ${r.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal
|
|
712
845
|
`
|
|
713
846
|
);
|
|
714
847
|
}).catch(fail)
|
|
715
848
|
);
|
|
716
849
|
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(
|
|
717
|
-
(opts) => void
|
|
850
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
718
851
|
const result = trace(
|
|
719
852
|
db,
|
|
720
853
|
{
|
|
@@ -739,7 +872,7 @@ function createProgram() {
|
|
|
739
872
|
);
|
|
740
873
|
const list = program.command("list");
|
|
741
874
|
list.command("repos").option("--workspace <path>").action(
|
|
742
|
-
(opts) => void
|
|
875
|
+
(opts) => void withReadOnlyWorkspace(
|
|
743
876
|
opts.workspace,
|
|
744
877
|
(db) => process.stdout.write(
|
|
745
878
|
renderJson(
|
|
@@ -753,8 +886,12 @@ function createProgram() {
|
|
|
753
886
|
).catch(fail)
|
|
754
887
|
);
|
|
755
888
|
list.command("services").option("--workspace <path>").option("--repo <name>").action(
|
|
756
|
-
(opts) => void
|
|
889
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
757
890
|
const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
|
|
891
|
+
if (opts.repo && !repo) {
|
|
892
|
+
process.stdout.write(renderJson([{ severity: "warning", code: "selector_repo_not_found", message: `Repository selector not found: ${opts.repo}` }]));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
758
895
|
const rows = db.prepare(
|
|
759
896
|
"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"
|
|
760
897
|
).all(repo?.id, repo?.id);
|
|
@@ -762,8 +899,12 @@ function createProgram() {
|
|
|
762
899
|
}).catch(fail)
|
|
763
900
|
);
|
|
764
901
|
list.command("operations").option("--workspace <path>").option("--repo <name>").option("--service <path>").action(
|
|
765
|
-
(opts) => void
|
|
902
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
766
903
|
const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
|
|
904
|
+
if (opts.repo && !repo) {
|
|
905
|
+
process.stdout.write(renderJson([{ severity: "warning", code: "selector_repo_not_found", message: `Repository selector not found: ${opts.repo}` }]));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
767
908
|
const rows = db.prepare(
|
|
768
909
|
"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=?)"
|
|
769
910
|
).all(repo?.id, repo?.id, opts.service, opts.service);
|
|
@@ -771,8 +912,12 @@ function createProgram() {
|
|
|
771
912
|
}).catch(fail)
|
|
772
913
|
);
|
|
773
914
|
list.command("calls").option("--workspace <path>").option("--repo <name>").option("--operation <name>").action(
|
|
774
|
-
(opts) => void
|
|
915
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
775
916
|
const repo = opts.repo ? repoByName(db, opts.repo) : void 0;
|
|
917
|
+
if (opts.repo && !repo) {
|
|
918
|
+
process.stdout.write(renderJson([{ severity: "warning", code: "selector_repo_not_found", message: `Repository selector not found: ${opts.repo}` }]));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
776
921
|
const rows = db.prepare(
|
|
777
922
|
"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 ?)"
|
|
778
923
|
).all(
|
|
@@ -786,8 +931,8 @@ function createProgram() {
|
|
|
786
931
|
process.stdout.write(renderJson(rows));
|
|
787
932
|
}).catch(fail)
|
|
788
933
|
);
|
|
789
|
-
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(
|
|
790
|
-
(opts) => void
|
|
934
|
+
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(
|
|
935
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
791
936
|
const result = trace(
|
|
792
937
|
db,
|
|
793
938
|
{
|
|
@@ -800,7 +945,8 @@ function createProgram() {
|
|
|
800
945
|
depth: 100,
|
|
801
946
|
includeAsync: true,
|
|
802
947
|
includeDb: true,
|
|
803
|
-
includeExternal: true
|
|
948
|
+
includeExternal: true,
|
|
949
|
+
vars: parseVars(opts.var)
|
|
804
950
|
}
|
|
805
951
|
);
|
|
806
952
|
process.stdout.write(
|
|
@@ -810,7 +956,7 @@ function createProgram() {
|
|
|
810
956
|
);
|
|
811
957
|
const inspect = program.command("inspect");
|
|
812
958
|
inspect.command("repo").argument("<name>").option("--workspace <path>").action(
|
|
813
|
-
(name, opts) => void
|
|
959
|
+
(name, opts) => void withReadOnlyWorkspace(
|
|
814
960
|
opts.workspace,
|
|
815
961
|
(db) => process.stdout.write(
|
|
816
962
|
renderJson(repoByName(db, name) ?? { error: "repo not found" })
|
|
@@ -818,7 +964,7 @@ function createProgram() {
|
|
|
818
964
|
).catch(fail)
|
|
819
965
|
);
|
|
820
966
|
inspect.command("operation").argument("<selector>").option("--workspace <path>").action(
|
|
821
|
-
(selector, opts) => void
|
|
967
|
+
(selector, opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
822
968
|
const rows = db.prepare(
|
|
823
969
|
"SELECT * FROM cds_operations WHERE operation_name=? OR operation_path=?"
|
|
824
970
|
).all(selector, selector);
|
|
@@ -826,24 +972,27 @@ function createProgram() {
|
|
|
826
972
|
}).catch(fail)
|
|
827
973
|
);
|
|
828
974
|
program.command("doctor").option("--workspace <path>").option("--strict").action(
|
|
829
|
-
(opts) => void
|
|
975
|
+
(opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
830
976
|
const diagnostics = db.prepare(
|
|
831
977
|
"SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id"
|
|
832
|
-
).all(
|
|
978
|
+
).all();
|
|
833
979
|
const health = db.prepare(
|
|
834
980
|
`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
|
|
835
981
|
FROM cds_services s LEFT JOIN cds_operations o ON o.service_id=s.id WHERE o.id IS NULL AND ?
|
|
836
982
|
UNION ALL
|
|
837
983
|
SELECT 'warning','extend_service_unresolved_base','Extend service has no indexed local operations; verify base service resolution',s.source_file,s.source_line
|
|
838
|
-
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
|
|
984
|
+
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 ?
|
|
839
985
|
UNION ALL
|
|
840
986
|
SELECT 'warning','handler_without_service','Repository has handlers but no CDS services',hc.source_file,hc.source_line
|
|
841
987
|
FROM handler_classes hc JOIN repositories r ON r.id=hc.repo_id
|
|
842
988
|
WHERE r.kind IN ('cap-service','mixed') AND NOT EXISTS (SELECT 1 FROM cds_services s WHERE s.repo_id=hc.repo_id)
|
|
843
989
|
UNION ALL
|
|
844
990
|
SELECT 'warning','search_index_empty','Search index is empty after indexing',NULL,NULL
|
|
845
|
-
WHERE NOT EXISTS (SELECT 1 FROM search_index)
|
|
846
|
-
|
|
991
|
+
WHERE NOT EXISTS (SELECT 1 FROM search_index)
|
|
992
|
+
UNION ALL
|
|
993
|
+
SELECT 'error','foreign_key_violation','SQLite foreign_key_check reported integrity failures',NULL,NULL
|
|
994
|
+
WHERE EXISTS (SELECT 1 FROM pragma_foreign_key_check)`
|
|
995
|
+
).all(Boolean(opts.strict), Boolean(opts.strict));
|
|
847
996
|
const allDiagnostics = [...diagnostics, ...health];
|
|
848
997
|
process.stdout.write(
|
|
849
998
|
allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
|