@saptools/service-flow 0.1.51 → 0.1.52
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 +7 -0
- package/README.md +10 -5
- package/TECHNICAL-NOTE.md +8 -4
- package/dist/{chunk-YZJKE5UX.js → chunk-PTLDSHRC.js} +2412 -553
- package/dist/chunk-PTLDSHRC.js.map +1 -0
- package/dist/cli.js +280 -506
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -7
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli/000-clean.ts +82 -0
- package/src/cli.ts +75 -57
- package/src/db/connection.ts +1 -1
- package/src/db/migrations.ts +5 -3
- package/src/db/repositories.ts +120 -22
- package/src/db/schema.ts +7 -2
- package/src/indexer/repository-indexer.ts +57 -29
- package/src/indexer/workspace-indexer.ts +84 -6
- package/src/linker/cross-repo-linker.ts +13 -1
- package/src/output/table-output.ts +29 -3
- package/src/parsers/cds-parser.ts +8 -2
- package/src/parsers/decorator-parser.ts +382 -48
- package/src/parsers/handler-registration-parser.ts +38 -21
- package/src/parsers/imported-wrapper-parser.ts +18 -5
- package/src/parsers/outbound-call-parser.ts +25 -8
- package/src/parsers/package-json-parser.ts +36 -11
- package/src/parsers/service-binding-parser-helpers.ts +8 -1
- package/src/parsers/service-binding-parser.ts +6 -3
- package/src/parsers/symbol-parser.ts +13 -3
- package/src/parsers/ts-project.ts +54 -0
- package/src/trace/000-dynamic-target-types.ts +84 -0
- package/src/trace/001-dynamic-identity.ts +280 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +82 -0
- package/src/trace/dynamic-targets.ts +459 -229
- package/src/trace/evidence.ts +305 -46
- package/src/trace/selectors.ts +483 -0
- package/src/trace/trace-engine.ts +90 -83
- package/src/types.ts +27 -1
- package/dist/chunk-YZJKE5UX.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
1
3
|
type CallType = 'remote_action' | 'remote_query' | 'remote_entity_read' | 'remote_entity_mutation' | 'remote_entity_delete' | 'remote_entity_media' | 'remote_entity_candidate' | 'local_db_query' | 'external_http' | 'async_emit' | 'async_subscribe' | 'local_service_call' | 'unknown';
|
|
2
4
|
interface DiscoveredRepository {
|
|
3
5
|
name: string;
|
|
@@ -55,17 +57,36 @@ interface HandlerClassFact {
|
|
|
55
57
|
sourceFile: string;
|
|
56
58
|
sourceLine: number;
|
|
57
59
|
methods: HandlerMethodFact[];
|
|
60
|
+
hasHandlerDecorator?: boolean;
|
|
61
|
+
classDecoratorNames?: string[];
|
|
62
|
+
observedDecoratorNames?: string[];
|
|
63
|
+
unsupportedDecoratorNames?: string[];
|
|
58
64
|
}
|
|
65
|
+
type HandlerMethodKind = 'operation' | 'entity_lifecycle' | 'event' | 'unsupported_lifecycle' | 'unsupported_decorator';
|
|
66
|
+
type HandlerLifecyclePhase = 'on' | 'before' | 'after';
|
|
67
|
+
type HandlerLifecycleEvent = 'CREATE' | 'READ' | 'UPDATE' | 'DELETE';
|
|
59
68
|
interface HandlerMethodFact {
|
|
60
69
|
methodName: string;
|
|
61
70
|
decoratorKind: string;
|
|
62
71
|
decoratorValue?: string;
|
|
63
72
|
decoratorRawExpression: string;
|
|
73
|
+
handlerKind?: HandlerMethodKind;
|
|
74
|
+
executable?: boolean;
|
|
75
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
76
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
64
77
|
decoratorResolution: {
|
|
65
78
|
rawExpression: string;
|
|
79
|
+
decoratorExpression?: string;
|
|
80
|
+
argumentExpression?: string;
|
|
81
|
+
resolvedDecoratorKind?: string;
|
|
82
|
+
decoratorImportSource?: string;
|
|
66
83
|
resolvedValue?: string;
|
|
67
|
-
resolutionKind: 'literal' | 'const_identifier' | 'enum_member' | 'const_object_property' | 'generated_constant_name' | 'unresolved';
|
|
84
|
+
resolutionKind: 'literal' | 'const_identifier' | 'enum_member' | 'const_object_property' | 'generated_constant_name' | 'lifecycle_implicit' | 'unresolved';
|
|
68
85
|
unresolvedReason?: string;
|
|
86
|
+
handlerKind?: HandlerMethodKind;
|
|
87
|
+
executable?: boolean;
|
|
88
|
+
lifecyclePhase?: HandlerLifecyclePhase;
|
|
89
|
+
lifecycleEvent?: HandlerLifecycleEvent;
|
|
69
90
|
};
|
|
70
91
|
sourceFile: string;
|
|
71
92
|
sourceLine: number;
|
|
@@ -138,6 +159,7 @@ interface ImplementationHint {
|
|
|
138
159
|
type DynamicMode = 'strict' | 'candidates' | 'infer';
|
|
139
160
|
interface TraceOptions {
|
|
140
161
|
depth: number;
|
|
162
|
+
workspaceId?: number;
|
|
141
163
|
vars?: Record<string, string>;
|
|
142
164
|
includeExternal?: boolean;
|
|
143
165
|
includeDb?: boolean;
|
|
@@ -165,17 +187,33 @@ interface TraceResult {
|
|
|
165
187
|
|
|
166
188
|
declare function discoverRepositories(rootPath: string, ignore: readonly string[]): Promise<DiscoveredRepository[]>;
|
|
167
189
|
|
|
168
|
-
|
|
190
|
+
interface ParsePackageJsonOptions {
|
|
191
|
+
strict?: boolean;
|
|
192
|
+
allowMissing?: boolean;
|
|
193
|
+
}
|
|
194
|
+
declare function parsePackageJson(repoPath: string, options?: ParsePackageJsonOptions): Promise<PackageFacts>;
|
|
195
|
+
|
|
196
|
+
interface SourceFileSnapshot {
|
|
197
|
+
repoPath: string;
|
|
198
|
+
filePath: string;
|
|
199
|
+
text: string;
|
|
200
|
+
sizeBytes: number;
|
|
201
|
+
sourceFile: () => ts.SourceFile;
|
|
202
|
+
}
|
|
203
|
+
interface RepositorySourceContext {
|
|
204
|
+
get: (filePath: string) => SourceFileSnapshot | undefined;
|
|
205
|
+
entries: () => SourceFileSnapshot[];
|
|
206
|
+
}
|
|
169
207
|
|
|
170
|
-
declare function parseCdsFile(repoPath: string, filePath: string): Promise<CdsServiceFact[]>;
|
|
208
|
+
declare function parseCdsFile(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<CdsServiceFact[]>;
|
|
171
209
|
|
|
172
|
-
declare function parseDecorators(repoPath: string, filePath: string): Promise<HandlerClassFact[]>;
|
|
210
|
+
declare function parseDecorators(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<HandlerClassFact[]>;
|
|
173
211
|
|
|
174
|
-
declare function parseHandlerRegistrations(repoPath: string, filePath: string): Promise<HandlerRegistrationFact[]>;
|
|
212
|
+
declare function parseHandlerRegistrations(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<HandlerRegistrationFact[]>;
|
|
175
213
|
|
|
176
|
-
declare function parseServiceBindings(repoPath: string, filePath: string): Promise<ServiceBindingFact[]>;
|
|
214
|
+
declare function parseServiceBindings(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<ServiceBindingFact[]>;
|
|
177
215
|
|
|
178
|
-
declare function parseOutboundCalls(repoPath: string, filePath: string): Promise<OutboundCallFact[]>;
|
|
216
|
+
declare function parseOutboundCalls(repoPath: string, filePath: string, context?: RepositorySourceContext): Promise<OutboundCallFact[]>;
|
|
179
217
|
|
|
180
218
|
declare function parseGeneratedConstants(repoPath: string, filePath: string): Promise<GeneratedConstantFact[]>;
|
|
181
219
|
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { WorkspaceConfig } from '../config/workspace-config.js';
|
|
4
|
+
import { openDatabase } from '../db/connection.js';
|
|
5
|
+
import { getWorkspace, upsertWorkspace } from '../db/repositories.js';
|
|
6
|
+
import { claimIndexRun } from '../indexer/workspace-indexer.js';
|
|
7
|
+
import { errorMessage } from '../utils/diagnostics.js';
|
|
8
|
+
|
|
9
|
+
type CleanConfig = Pick<WorkspaceConfig, 'dbPath' | 'rootPath'>;
|
|
10
|
+
|
|
11
|
+
export async function cleanWorkspaceState(
|
|
12
|
+
config: CleanConfig,
|
|
13
|
+
dbOnly: boolean,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const dbDir = path.resolve(path.dirname(config.dbPath));
|
|
16
|
+
if (!dbOnly) await assertOwnedStateDirectory(dbDir, config.rootPath);
|
|
17
|
+
const runId = claimCleanWriter(config);
|
|
18
|
+
try {
|
|
19
|
+
if (dbOnly) await removeDatabaseFiles(config.dbPath);
|
|
20
|
+
else await fs.rm(dbDir, { recursive: true, force: true });
|
|
21
|
+
} catch (error) {
|
|
22
|
+
await markCleanClaimFailed(config.dbPath, runId, error);
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function claimCleanWriter(config: CleanConfig): number {
|
|
28
|
+
const db = openDatabase(config.dbPath);
|
|
29
|
+
try {
|
|
30
|
+
const workspaceId = getWorkspace(db, config.rootPath)?.id
|
|
31
|
+
?? upsertWorkspace(db, config.rootPath, config.dbPath);
|
|
32
|
+
return claimIndexRun(db, workspaceId, 0);
|
|
33
|
+
} finally {
|
|
34
|
+
db.close();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertOwnedStateDirectory(
|
|
39
|
+
dbDir: string,
|
|
40
|
+
rootPath: string,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const marker = path.join(dbDir, '.service-flow-state');
|
|
43
|
+
const dangerous = new Set([
|
|
44
|
+
path.parse(dbDir).root,
|
|
45
|
+
'/tmp',
|
|
46
|
+
process.env.HOME ? path.resolve(process.env.HOME) : '',
|
|
47
|
+
path.resolve(rootPath),
|
|
48
|
+
]);
|
|
49
|
+
const ownsState = await fs.stat(marker)
|
|
50
|
+
.then((stat) => stat.isFile())
|
|
51
|
+
.catch(() => false);
|
|
52
|
+
if (!ownsState || dangerous.has(dbDir))
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function removeDatabaseFiles(dbPath: string): Promise<void> {
|
|
59
|
+
for (const suffix of ['-wal', '-shm', '-journal'])
|
|
60
|
+
await fs.rm(`${dbPath}${suffix}`, { force: true });
|
|
61
|
+
await fs.rm(dbPath, { force: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function markCleanClaimFailed(
|
|
65
|
+
dbPath: string,
|
|
66
|
+
runId: number,
|
|
67
|
+
error: unknown,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const exists = await fs.stat(dbPath).then(() => true).catch(() => false);
|
|
70
|
+
if (!exists) return;
|
|
71
|
+
const db = openDatabase(dbPath);
|
|
72
|
+
try {
|
|
73
|
+
db.prepare(`UPDATE index_runs SET finished_at=?,status='failed',
|
|
74
|
+
error_message=? WHERE id=? AND status='running'`).run(
|
|
75
|
+
new Date().toISOString(),
|
|
76
|
+
`Clean failed after writer claim: ${errorMessage(error)}`,
|
|
77
|
+
runId,
|
|
78
|
+
);
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import fs from 'node:fs/promises';
|
|
4
2
|
import { DEFAULT_IGNORES } from './config/defaults.js';
|
|
5
3
|
import {
|
|
6
4
|
createWorkspaceConfig,
|
|
7
5
|
loadWorkspaceConfig,
|
|
8
6
|
saveWorkspaceConfig,
|
|
9
7
|
} from './config/workspace-config.js';
|
|
10
|
-
import { openDatabase, openReadOnlyDatabase } from './db/connection.js';
|
|
8
|
+
import { openDatabase, openReadOnlyDatabase, type Db } from './db/connection.js';
|
|
11
9
|
import {
|
|
12
10
|
getWorkspace,
|
|
13
11
|
listRepositories,
|
|
14
|
-
|
|
12
|
+
reposByName,
|
|
13
|
+
type RepoRow,
|
|
15
14
|
upsertRepository,
|
|
16
15
|
upsertWorkspace,
|
|
17
16
|
} from './db/repositories.js';
|
|
@@ -22,7 +21,10 @@ import { indexWorkspace } from './indexer/workspace-indexer.js';
|
|
|
22
21
|
import { linkWorkspace } from './linker/cross-repo-linker.js';
|
|
23
22
|
import { doctorDiagnostics, linkUpgradeWarnings } from './cli/doctor.js';
|
|
24
23
|
import { trace } from './trace/trace-engine.js';
|
|
25
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
parseVars,
|
|
26
|
+
selectorRepoAmbiguousDiagnostic,
|
|
27
|
+
} from './trace/selectors.js';
|
|
26
28
|
import { parseImplementationHint } from './trace/implementation-hints.js';
|
|
27
29
|
import { renderTraceTable } from './output/table-output.js';
|
|
28
30
|
import { renderTraceJson, renderJson } from './output/json-output.js';
|
|
@@ -30,6 +32,7 @@ import { renderDoctorDiagnostics } from './output/doctor-output.js';
|
|
|
30
32
|
import { renderMermaid } from './output/mermaid-output.js';
|
|
31
33
|
import { VERSION } from './version.js';
|
|
32
34
|
import type { DynamicMode } from './types.js';
|
|
35
|
+
import { cleanWorkspaceState } from './cli/000-clean.js';
|
|
33
36
|
async function init(
|
|
34
37
|
workspace: string,
|
|
35
38
|
options: { db?: string; ignore?: string[] },
|
|
@@ -92,6 +95,30 @@ async function withReadOnlyWorkspace<T>(
|
|
|
92
95
|
db.close();
|
|
93
96
|
}
|
|
94
97
|
}
|
|
98
|
+
function selectRepository(db: Db, selector: string, workspaceId?: number): {
|
|
99
|
+
repo?: RepoRow;
|
|
100
|
+
diagnostic?: Record<string, unknown>;
|
|
101
|
+
} {
|
|
102
|
+
const candidates = reposByName(db, selector, workspaceId);
|
|
103
|
+
if (candidates.length === 1) return { repo: candidates[0] };
|
|
104
|
+
if (candidates.length === 0) return {
|
|
105
|
+
diagnostic: {
|
|
106
|
+
severity: 'warning',
|
|
107
|
+
code: 'selector_repo_not_found',
|
|
108
|
+
message: `Repository selector not found: ${selector}`,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
diagnostic: selectorRepoAmbiguousDiagnostic(
|
|
113
|
+
selector,
|
|
114
|
+
candidates.map((repo) => ({
|
|
115
|
+
id: repo.id,
|
|
116
|
+
name: repo.name,
|
|
117
|
+
packageName: repo.package_name ?? undefined,
|
|
118
|
+
})),
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
95
122
|
export function createProgram(): Command {
|
|
96
123
|
const program = new Command();
|
|
97
124
|
program
|
|
@@ -177,7 +204,7 @@ export function createProgram(): Command {
|
|
|
177
204
|
dynamicMode: string;
|
|
178
205
|
maxDynamicCandidates: string;
|
|
179
206
|
}) =>
|
|
180
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
207
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
181
208
|
const result = trace(
|
|
182
209
|
db,
|
|
183
210
|
{
|
|
@@ -189,6 +216,7 @@ export function createProgram(): Command {
|
|
|
189
216
|
},
|
|
190
217
|
{
|
|
191
218
|
depth: Number(opts.depth),
|
|
219
|
+
workspaceId,
|
|
192
220
|
vars: parseVars(opts.var),
|
|
193
221
|
includeExternal: Boolean(opts.includeExternal),
|
|
194
222
|
includeDb: Boolean(opts.includeDb),
|
|
@@ -214,10 +242,10 @@ export function createProgram(): Command {
|
|
|
214
242
|
.option('--workspace <path>')
|
|
215
243
|
.action(
|
|
216
244
|
(opts: { workspace?: string }) =>
|
|
217
|
-
void withReadOnlyWorkspace(opts.workspace, (db) =>
|
|
245
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
|
|
218
246
|
process.stdout.write(
|
|
219
247
|
renderJson(
|
|
220
|
-
listRepositories(db).map((r) => ({
|
|
248
|
+
listRepositories(db, workspaceId).map((r) => ({
|
|
221
249
|
name: r.name,
|
|
222
250
|
kind: r.kind,
|
|
223
251
|
packageName: r.package_name,
|
|
@@ -232,17 +260,20 @@ export function createProgram(): Command {
|
|
|
232
260
|
.option('--repo <name>')
|
|
233
261
|
.action(
|
|
234
262
|
(opts: { workspace?: string; repo?: string }) =>
|
|
235
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
263
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
264
|
+
const selection = opts.repo
|
|
265
|
+
? selectRepository(db, opts.repo, workspaceId)
|
|
266
|
+
: {};
|
|
267
|
+
if (selection.diagnostic) {
|
|
268
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
239
269
|
return;
|
|
240
270
|
}
|
|
271
|
+
const repo = selection.repo;
|
|
241
272
|
const rows = db
|
|
242
273
|
.prepare(
|
|
243
|
-
'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',
|
|
274
|
+
'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 r.workspace_id=? AND (? IS NULL OR s.repo_id=?) ORDER BY r.name,s.service_path',
|
|
244
275
|
)
|
|
245
|
-
.all(repo?.id, repo?.id);
|
|
276
|
+
.all(workspaceId, repo?.id, repo?.id);
|
|
246
277
|
process.stdout.write(renderJson(rows));
|
|
247
278
|
}).catch(fail),
|
|
248
279
|
);
|
|
@@ -253,17 +284,20 @@ export function createProgram(): Command {
|
|
|
253
284
|
.option('--service <path>')
|
|
254
285
|
.action(
|
|
255
286
|
(opts: { workspace?: string; repo?: string; service?: string }) =>
|
|
256
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
287
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
288
|
+
const selection = opts.repo
|
|
289
|
+
? selectRepository(db, opts.repo, workspaceId)
|
|
290
|
+
: {};
|
|
291
|
+
if (selection.diagnostic) {
|
|
292
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
260
293
|
return;
|
|
261
294
|
}
|
|
295
|
+
const repo = selection.repo;
|
|
262
296
|
const rows = db
|
|
263
297
|
.prepare(
|
|
264
|
-
'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=?)',
|
|
298
|
+
'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 r.workspace_id=? AND (? IS NULL OR s.repo_id=?) AND (? IS NULL OR s.service_path=?)',
|
|
265
299
|
)
|
|
266
|
-
.all(repo?.id, repo?.id, opts.service, opts.service);
|
|
300
|
+
.all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
|
|
267
301
|
process.stdout.write(renderJson(rows));
|
|
268
302
|
}).catch(fail),
|
|
269
303
|
);
|
|
@@ -274,17 +308,21 @@ export function createProgram(): Command {
|
|
|
274
308
|
.option('--operation <name>')
|
|
275
309
|
.action(
|
|
276
310
|
(opts: { workspace?: string; repo?: string; operation?: string }) =>
|
|
277
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
312
|
+
const selection = opts.repo
|
|
313
|
+
? selectRepository(db, opts.repo, workspaceId)
|
|
314
|
+
: {};
|
|
315
|
+
if (selection.diagnostic) {
|
|
316
|
+
process.stdout.write(renderJson([selection.diagnostic]));
|
|
281
317
|
return;
|
|
282
318
|
}
|
|
319
|
+
const repo = selection.repo;
|
|
283
320
|
const rows = db
|
|
284
321
|
.prepare(
|
|
285
|
-
'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 ?)',
|
|
322
|
+
'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 r.workspace_id=? AND (? IS NULL OR c.repo_id=?) AND (? IS NULL OR c.operation_path_expr=? OR c.operation_path_expr=? OR c.payload_summary LIKE ?)',
|
|
286
323
|
)
|
|
287
324
|
.all(
|
|
325
|
+
workspaceId,
|
|
288
326
|
repo?.id,
|
|
289
327
|
repo?.id,
|
|
290
328
|
opts.operation,
|
|
@@ -322,7 +360,7 @@ export function createProgram(): Command {
|
|
|
322
360
|
dynamicMode: string;
|
|
323
361
|
maxDynamicCandidates: string;
|
|
324
362
|
}) =>
|
|
325
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
363
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
326
364
|
const result = trace(
|
|
327
365
|
db,
|
|
328
366
|
{
|
|
@@ -333,6 +371,7 @@ export function createProgram(): Command {
|
|
|
333
371
|
},
|
|
334
372
|
{
|
|
335
373
|
depth: 100,
|
|
374
|
+
workspaceId,
|
|
336
375
|
includeAsync: true,
|
|
337
376
|
includeDb: true,
|
|
338
377
|
includeExternal: true,
|
|
@@ -357,11 +396,12 @@ export function createProgram(): Command {
|
|
|
357
396
|
.option('--workspace <path>')
|
|
358
397
|
.action(
|
|
359
398
|
(name: string, opts: { workspace?: string }) =>
|
|
360
|
-
void withReadOnlyWorkspace(opts.workspace, (db) =>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
399
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
400
|
+
const selection = selectRepository(db, name, workspaceId);
|
|
401
|
+
process.stdout.write(renderJson(
|
|
402
|
+
selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
|
|
403
|
+
));
|
|
404
|
+
}).catch(fail),
|
|
365
405
|
);
|
|
366
406
|
inspect
|
|
367
407
|
.command('operation')
|
|
@@ -369,12 +409,12 @@ export function createProgram(): Command {
|
|
|
369
409
|
.option('--workspace <path>')
|
|
370
410
|
.action(
|
|
371
411
|
(selector: string, opts: { workspace?: string }) =>
|
|
372
|
-
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
412
|
+
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
373
413
|
const rows = db
|
|
374
414
|
.prepare(
|
|
375
|
-
'SELECT
|
|
415
|
+
'SELECT o.* FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND (o.operation_name=? OR o.operation_path=?)',
|
|
376
416
|
)
|
|
377
|
-
.all(selector, selector);
|
|
417
|
+
.all(workspaceId, selector, selector);
|
|
378
418
|
process.stdout.write(renderJson(rows));
|
|
379
419
|
}).catch(fail),
|
|
380
420
|
);
|
|
@@ -399,29 +439,7 @@ export function createProgram(): Command {
|
|
|
399
439
|
(opts: { workspace?: string; dbOnly?: boolean }) =>
|
|
400
440
|
void (async () => {
|
|
401
441
|
const config = await loadWorkspaceConfig(opts.workspace);
|
|
402
|
-
|
|
403
|
-
const workspaceRoot = path.resolve(config.rootPath);
|
|
404
|
-
await fs.rm(config.dbPath, { force: true });
|
|
405
|
-
if (!opts.dbOnly) {
|
|
406
|
-
const marker = path.join(dbDir, '.service-flow-state');
|
|
407
|
-
const dangerous = new Set([
|
|
408
|
-
path.parse(dbDir).root,
|
|
409
|
-
'/tmp',
|
|
410
|
-
process.env.HOME ? path.resolve(process.env.HOME) : '',
|
|
411
|
-
workspaceRoot,
|
|
412
|
-
]);
|
|
413
|
-
let ownsState: boolean;
|
|
414
|
-
try {
|
|
415
|
-
ownsState = (await fs.stat(marker)).isFile();
|
|
416
|
-
} catch {
|
|
417
|
-
ownsState = false;
|
|
418
|
-
}
|
|
419
|
-
if (!ownsState || dangerous.has(dbDir))
|
|
420
|
-
throw new Error(
|
|
421
|
-
`Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`,
|
|
422
|
-
);
|
|
423
|
-
await fs.rm(dbDir, { recursive: true, force: true });
|
|
424
|
-
}
|
|
442
|
+
await cleanWorkspaceState(config, Boolean(opts.dbOnly));
|
|
425
443
|
process.stdout.write('Cleaned service-flow state\n');
|
|
426
444
|
})().catch(fail),
|
|
427
445
|
);
|
package/src/db/connection.ts
CHANGED
|
@@ -101,8 +101,8 @@ export function openDatabase(dbPath: string, options: OpenDatabaseOptions = {}):
|
|
|
101
101
|
},
|
|
102
102
|
transaction<T>(fn: () => T): T {
|
|
103
103
|
if (inTransaction) return fn();
|
|
104
|
-
inTransaction = true;
|
|
105
104
|
native.exec('BEGIN IMMEDIATE');
|
|
105
|
+
inTransaction = true;
|
|
106
106
|
try {
|
|
107
107
|
const result = fn();
|
|
108
108
|
native.exec('COMMIT');
|
package/src/db/migrations.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Db } from './connection.js';
|
|
2
|
-
import {
|
|
3
|
-
const CURRENT_SCHEMA_VERSION =
|
|
2
|
+
import { schemaIndexesSql, schemaTablesSql } from './schema.js';
|
|
3
|
+
const CURRENT_SCHEMA_VERSION = 11;
|
|
4
4
|
const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
5
5
|
handler_methods: [
|
|
6
6
|
{ name: 'decorator_resolution_json', ddl: "ALTER TABLE handler_methods ADD COLUMN decorator_resolution_json TEXT NOT NULL DEFAULT '{}'" },
|
|
@@ -57,6 +57,7 @@ const columns: Record<string, Array<{ name: string; ddl: string }>> = {
|
|
|
57
57
|
],
|
|
58
58
|
index_runs: [
|
|
59
59
|
{ name: 'error_message', ddl: 'ALTER TABLE index_runs ADD COLUMN error_message TEXT' },
|
|
60
|
+
{ name: 'owner_pid', ddl: 'ALTER TABLE index_runs ADD COLUMN owner_pid INTEGER' },
|
|
60
61
|
],
|
|
61
62
|
};
|
|
62
63
|
function hasColumn(db: Db, table: string, column: string): boolean {
|
|
@@ -81,8 +82,9 @@ export function migrate(db: Db): void {
|
|
|
81
82
|
db.transaction(() => {
|
|
82
83
|
const version = userVersion(db);
|
|
83
84
|
if (version > CURRENT_SCHEMA_VERSION) throw new Error(`Unsupported future service-flow schema version ${version}`);
|
|
84
|
-
db.exec(
|
|
85
|
+
db.exec(schemaTablesSql);
|
|
85
86
|
addMissingColumns(db);
|
|
87
|
+
db.exec(schemaIndexesSql);
|
|
86
88
|
normalizeLegacyStatus(db);
|
|
87
89
|
const violations = db.pragma('foreign_key_check');
|
|
88
90
|
if (violations.length > 0) throw new Error('SQLite foreign_key_check failed during migration');
|
package/src/db/repositories.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
CdsRequire,
|
|
4
4
|
CdsServiceFact,
|
|
5
5
|
HandlerClassFact,
|
|
6
|
+
HandlerMethodFact,
|
|
6
7
|
HandlerRegistrationFact,
|
|
7
8
|
OutboundCallFact,
|
|
8
9
|
ServiceBindingFact,
|
|
@@ -85,15 +86,29 @@ export function upsertRepository(
|
|
|
85
86
|
.get(workspaceId, r.absolutePath)?.id,
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
|
-
export function listRepositories(db: Db): RepoRow[] {
|
|
89
|
+
export function listRepositories(db: Db, workspaceId?: number): RepoRow[] {
|
|
89
90
|
return db
|
|
90
|
-
.prepare('SELECT * FROM repositories ORDER BY name')
|
|
91
|
-
.all() as unknown as RepoRow[];
|
|
91
|
+
.prepare('SELECT * FROM repositories WHERE (? IS NULL OR workspace_id=?) ORDER BY name,absolute_path,id')
|
|
92
|
+
.all(workspaceId, workspaceId) as unknown as RepoRow[];
|
|
92
93
|
}
|
|
93
|
-
export function repoByName(
|
|
94
|
+
export function repoByName(
|
|
95
|
+
db: Db,
|
|
96
|
+
name: string,
|
|
97
|
+
workspaceId?: number,
|
|
98
|
+
): RepoRow | undefined {
|
|
99
|
+
const matches = reposByName(db, name, workspaceId);
|
|
100
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
101
|
+
}
|
|
102
|
+
export function reposByName(
|
|
103
|
+
db: Db,
|
|
104
|
+
name: string,
|
|
105
|
+
workspaceId?: number,
|
|
106
|
+
): RepoRow[] {
|
|
94
107
|
return db
|
|
95
|
-
.prepare(
|
|
96
|
-
|
|
108
|
+
.prepare(`SELECT * FROM repositories
|
|
109
|
+
WHERE (? IS NULL OR workspace_id=?) AND (name=? OR package_name=?)
|
|
110
|
+
ORDER BY name,absolute_path,id`)
|
|
111
|
+
.all(workspaceId, workspaceId, name, name) as unknown as RepoRow[];
|
|
97
112
|
}
|
|
98
113
|
export function clearRepoFacts(db: Db, repoId: number): void {
|
|
99
114
|
for (const t of [
|
|
@@ -188,21 +203,7 @@ export function insertHandler(
|
|
|
188
203
|
repoId: number,
|
|
189
204
|
h: HandlerClassFact,
|
|
190
205
|
): number {
|
|
191
|
-
const sid =
|
|
192
|
-
db
|
|
193
|
-
.prepare(
|
|
194
|
-
'INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line) VALUES(?,?,?,?,?,?,?) RETURNING id',
|
|
195
|
-
)
|
|
196
|
-
.get(
|
|
197
|
-
repoId,
|
|
198
|
-
'class',
|
|
199
|
-
h.className,
|
|
200
|
-
h.className,
|
|
201
|
-
1,
|
|
202
|
-
h.sourceLine,
|
|
203
|
-
h.sourceLine,
|
|
204
|
-
)?.id,
|
|
205
|
-
);
|
|
206
|
+
const sid = insertHandlerClassSymbol(db, repoId, h);
|
|
206
207
|
const hid = Number(
|
|
207
208
|
db
|
|
208
209
|
.prepare(
|
|
@@ -220,12 +221,109 @@ export function insertHandler(
|
|
|
220
221
|
m.decoratorKind,
|
|
221
222
|
m.decoratorValue,
|
|
222
223
|
m.decoratorRawExpression,
|
|
223
|
-
JSON.stringify(m
|
|
224
|
+
JSON.stringify(canonicalHandlerMethodResolution(m)),
|
|
224
225
|
m.sourceFile,
|
|
225
226
|
m.sourceLine,
|
|
226
227
|
);
|
|
228
|
+
insertHandlerIndexDiagnostic(db, repoId, h);
|
|
227
229
|
return hid;
|
|
228
230
|
}
|
|
231
|
+
function insertHandlerClassSymbol(
|
|
232
|
+
db: Db,
|
|
233
|
+
repoId: number,
|
|
234
|
+
h: HandlerClassFact,
|
|
235
|
+
): number {
|
|
236
|
+
const classEvidence = {
|
|
237
|
+
hasHandlerDecorator: h.hasHandlerDecorator ?? false,
|
|
238
|
+
classDecoratorNames: h.classDecoratorNames ?? [],
|
|
239
|
+
observedDecoratorNames: h.observedDecoratorNames ?? [],
|
|
240
|
+
unsupportedDecoratorNames: h.unsupportedDecoratorNames ?? [],
|
|
241
|
+
unsupportedMethods: h.methods
|
|
242
|
+
.filter((method) => !handlerMethodIsExecutable(method))
|
|
243
|
+
.map((method) => ({
|
|
244
|
+
methodName: method.methodName,
|
|
245
|
+
decoratorKind: method.decoratorKind,
|
|
246
|
+
sourceFile: method.sourceFile,
|
|
247
|
+
sourceLine: method.sourceLine,
|
|
248
|
+
reason: method.decoratorResolution.unresolvedReason,
|
|
249
|
+
})),
|
|
250
|
+
};
|
|
251
|
+
return Number(
|
|
252
|
+
db
|
|
253
|
+
.prepare(
|
|
254
|
+
'INSERT INTO symbols(repo_id,kind,name,qualified_name,exported,start_line,end_line,source_file,evidence_json) VALUES(?,?,?,?,?,?,?,?,?) RETURNING id',
|
|
255
|
+
)
|
|
256
|
+
.get(
|
|
257
|
+
repoId,
|
|
258
|
+
'class',
|
|
259
|
+
h.className,
|
|
260
|
+
h.className,
|
|
261
|
+
1,
|
|
262
|
+
h.sourceLine,
|
|
263
|
+
h.sourceLine,
|
|
264
|
+
h.sourceFile,
|
|
265
|
+
JSON.stringify(classEvidence),
|
|
266
|
+
)?.id,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
function insertHandlerIndexDiagnostic(
|
|
270
|
+
db: Db,
|
|
271
|
+
repoId: number,
|
|
272
|
+
h: HandlerClassFact,
|
|
273
|
+
): void {
|
|
274
|
+
if (!h.hasHandlerDecorator) return;
|
|
275
|
+
const hasExecutable = h.methods.some(handlerMethodIsExecutable);
|
|
276
|
+
const unsupported = h.methods.filter((method) =>
|
|
277
|
+
!handlerMethodIsExecutable(method));
|
|
278
|
+
if (hasExecutable && unsupported.length === 0) return;
|
|
279
|
+
const code = hasExecutable
|
|
280
|
+
? 'handler_decorators_not_indexed'
|
|
281
|
+
: 'handler_methods_not_indexed';
|
|
282
|
+
const names = unsupported.map((method) => method.decoratorKind).sort();
|
|
283
|
+
const detail = names.length > 0
|
|
284
|
+
? ` Unsupported decorators: ${[...new Set(names)].join(', ')}.`
|
|
285
|
+
: '';
|
|
286
|
+
db.prepare(
|
|
287
|
+
'INSERT INTO diagnostics(repo_id,severity,code,message,source_file,source_line) VALUES(?,?,?,?,?,?)',
|
|
288
|
+
).run(
|
|
289
|
+
repoId,
|
|
290
|
+
'warning',
|
|
291
|
+
code,
|
|
292
|
+
hasExecutable
|
|
293
|
+
? `Handler class ${h.className} contains methods that were not indexed.${detail}`
|
|
294
|
+
: `Handler class ${h.className} has no indexed executable methods; use a supported CAP handler decorator and re-index.${detail}`,
|
|
295
|
+
h.sourceFile,
|
|
296
|
+
h.sourceLine,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
export function canonicalHandlerMethodResolution(
|
|
300
|
+
method: HandlerMethodFact,
|
|
301
|
+
): HandlerMethodFact['decoratorResolution'] {
|
|
302
|
+
const handlerKind = method.handlerKind
|
|
303
|
+
?? method.decoratorResolution.handlerKind
|
|
304
|
+
?? legacyHandlerKind(method.decoratorKind);
|
|
305
|
+
const executable = method.executable
|
|
306
|
+
?? method.decoratorResolution.executable
|
|
307
|
+
?? (handlerKind === 'operation' || handlerKind === 'event'
|
|
308
|
+
|| handlerKind === 'entity_lifecycle');
|
|
309
|
+
return {
|
|
310
|
+
...method.decoratorResolution,
|
|
311
|
+
handlerKind,
|
|
312
|
+
executable,
|
|
313
|
+
lifecyclePhase: method.lifecyclePhase
|
|
314
|
+
?? method.decoratorResolution.lifecyclePhase,
|
|
315
|
+
lifecycleEvent: method.lifecycleEvent
|
|
316
|
+
?? method.decoratorResolution.lifecycleEvent,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export function handlerMethodIsExecutable(method: HandlerMethodFact): boolean {
|
|
320
|
+
return canonicalHandlerMethodResolution(method).executable === true;
|
|
321
|
+
}
|
|
322
|
+
function legacyHandlerKind(kind: string): HandlerMethodFact['handlerKind'] {
|
|
323
|
+
if (kind === 'Event') return 'event';
|
|
324
|
+
if (['Action', 'Func', 'On'].includes(kind)) return 'operation';
|
|
325
|
+
return 'unsupported_decorator';
|
|
326
|
+
}
|
|
229
327
|
export function insertRegistrations(
|
|
230
328
|
db: Db,
|
|
231
329
|
repoId: number,
|