@saptools/service-flow 0.1.42 → 0.1.43
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/dist/{chunk-RP3BJ64F.js → chunk-KHQK7CFH.js} +22 -17
- package/dist/chunk-KHQK7CFH.js.map +1 -0
- package/dist/cli.js +51 -21
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/indexer/cds-extension-resolver.ts +58 -19
- package/src/parsers/outbound-call-parser.ts +21 -16
- package/dist/chunk-RP3BJ64F.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -12,40 +12,79 @@ interface ExtensionRow {
|
|
|
12
12
|
}
|
|
13
13
|
interface BaseRow { id: number; repoId: number }
|
|
14
14
|
|
|
15
|
+
interface DesiredOperation { id: number; operationType: string; operationName: string; operationPath: string; paramsJson: string; returnType: string | null; sourceFile: string; sourceLine: number }
|
|
16
|
+
interface ExistingOperation extends DesiredOperation { baseOperationId: number | null }
|
|
17
|
+
|
|
15
18
|
export function materializeCdsExtensionOperations(db: Db, workspaceId: number): void {
|
|
16
19
|
const extensions = db.prepare(`SELECT s.id,r.id repoId,s.service_name serviceName,s.qualified_name qualifiedName,s.source_file sourceFile,s.extension_module_specifier moduleSpecifier,s.extension_imported_symbol importedSymbol,s.extension_import_kind importKind
|
|
17
20
|
FROM cds_services s JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=? AND s.is_extend=1`).all(workspaceId) as unknown as ExtensionRow[];
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
if (bases.length !== 1) {
|
|
23
|
-
db.prepare("DELETE FROM cds_operations WHERE service_id=? AND provenance='inherited'").run(extension.id);
|
|
24
|
-
db.prepare("DELETE FROM search_index WHERE repo=? AND kind='operation' AND name NOT IN (SELECT operation_name FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE s.repo_id=?)").run(String(extension.repoId), extension.repoId);
|
|
25
|
-
continue;
|
|
21
|
+
db.transaction(() => {
|
|
22
|
+
const changedRepos = new Set<number>();
|
|
23
|
+
for (const extension of extensions) {
|
|
24
|
+
if (reconcileExtension(db, workspaceId, extension)) changedRepos.add(extension.repoId);
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
}
|
|
26
|
+
for (const repoId of changedRepos) markRepositoryDerivedFactsChanged(db, repoId);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function reconcileExtension(db: Db, workspaceId: number, extension: ExtensionRow): boolean {
|
|
31
|
+
const bases = resolveBase(db, workspaceId, extension);
|
|
32
|
+
const status = bases.length === 1 ? 'resolved' : bases.length > 1 ? 'ambiguous' : 'unresolved';
|
|
33
|
+
const baseId = status === 'resolved' ? bases[0]?.id ?? null : null;
|
|
34
|
+
const desired = baseId === null ? [] : desiredInheritedOperations(db, extension, baseId);
|
|
35
|
+
const statusChanged = updateExtensionStatus(db, extension.id, status, baseId);
|
|
36
|
+
const operationChanged = reconcileInheritedOperations(db, extension, desired);
|
|
37
|
+
reconcileOperationSearchRows(db, extension.repoId);
|
|
38
|
+
return statusChanged || operationChanged;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function desiredInheritedOperations(db: Db, extension: ExtensionRow, baseId: number): DesiredOperation[] {
|
|
42
|
+
return db.prepare("SELECT id,operation_type operationType,operation_name operationName,operation_path operationPath,params_json paramsJson,return_type returnType,source_file sourceFile,source_line sourceLine FROM cds_operations WHERE service_id=? AND provenance='direct' AND NOT EXISTS (SELECT 1 FROM cds_operations direct WHERE direct.service_id=? AND direct.provenance='direct' AND direct.operation_name=cds_operations.operation_name AND direct.operation_path=cds_operations.operation_path) ORDER BY operation_name,operation_path,id").all(baseId, extension.id) as unknown as DesiredOperation[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function updateExtensionStatus(db: Db, serviceId: number, status: string, baseId: number | null): boolean {
|
|
46
|
+
const row = db.prepare('SELECT extension_base_status status,extension_base_service_id baseId FROM cds_services WHERE id=?').get(serviceId) as { status?: string | null; baseId?: number | null } | undefined;
|
|
47
|
+
if (row?.status === status && (row.baseId ?? null) === baseId) return false;
|
|
48
|
+
db.prepare('UPDATE cds_services SET extension_base_status=?, extension_base_service_id=? WHERE id=?').run(status, baseId, serviceId);
|
|
49
|
+
return true;
|
|
29
50
|
}
|
|
30
51
|
|
|
31
|
-
function reconcileInheritedOperations(db: Db, extension: ExtensionRow,
|
|
32
|
-
const existing = db.prepare("SELECT id,operation_name operationName,operation_path operationPath,base_operation_id baseOperationId FROM cds_operations WHERE service_id=? AND provenance='inherited'").all(extension.id) as
|
|
33
|
-
const
|
|
34
|
-
const desiredKeys = new Set(desired.map((row) => `${String(row.operationName)}\0${String(row.operationPath)}`));
|
|
52
|
+
function reconcileInheritedOperations(db: Db, extension: ExtensionRow, desired: DesiredOperation[]): boolean {
|
|
53
|
+
const existing = db.prepare("SELECT id,operation_type operationType,operation_name operationName,operation_path operationPath,params_json paramsJson,return_type returnType,source_file sourceFile,source_line sourceLine,base_operation_id baseOperationId FROM cds_operations WHERE service_id=? AND provenance='inherited'").all(extension.id) as unknown as ExistingOperation[];
|
|
54
|
+
const desiredKeys = new Set(desired.map((row) => `${row.operationName}\0${row.operationPath}`));
|
|
35
55
|
const byKey = new Map(existing.map((row) => [`${row.operationName}\0${row.operationPath}`, row]));
|
|
56
|
+
let changed = false;
|
|
36
57
|
for (const row of existing) {
|
|
37
|
-
if (
|
|
58
|
+
if (desiredKeys.has(`${row.operationName}\0${row.operationPath}`)) continue;
|
|
59
|
+
db.prepare('DELETE FROM cds_operations WHERE id=?').run(row.id);
|
|
60
|
+
changed = true;
|
|
38
61
|
}
|
|
39
62
|
const update = db.prepare('UPDATE cds_operations SET operation_type=?,params_json=?,return_type=?,source_file=?,source_line=?,base_operation_id=? WHERE id=?');
|
|
40
63
|
const add = db.prepare("INSERT INTO cds_operations(service_id,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line,provenance,base_operation_id) VALUES(?,?,?,?,?,?,?,?,'inherited',?)");
|
|
41
|
-
const search = db.prepare('INSERT INTO search_index(kind,name,path,repo) SELECT ?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM search_index WHERE kind=? AND name=? AND path=? AND repo=?)');
|
|
42
64
|
for (const row of desired) {
|
|
43
|
-
const
|
|
44
|
-
|
|
65
|
+
const current = byKey.get(`${row.operationName}\0${row.operationPath}`);
|
|
66
|
+
if (current && operationMatches(current, row)) continue;
|
|
45
67
|
if (current) update.run(row.operationType, row.paramsJson, row.returnType, row.sourceFile, row.sourceLine, row.id, current.id);
|
|
46
68
|
else add.run(extension.id, row.operationType, row.operationName, row.operationPath, row.paramsJson, row.returnType, row.sourceFile, row.sourceLine, row.id);
|
|
47
|
-
|
|
69
|
+
changed = true;
|
|
48
70
|
}
|
|
71
|
+
return changed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function operationMatches(current: ExistingOperation, desired: DesiredOperation): boolean {
|
|
75
|
+
return current.operationType === desired.operationType && current.paramsJson === desired.paramsJson && current.returnType === desired.returnType && current.sourceFile === desired.sourceFile && current.sourceLine === desired.sourceLine && current.baseOperationId === desired.id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reconcileOperationSearchRows(db: Db, repoId: number): void {
|
|
79
|
+
db.prepare("DELETE FROM search_index WHERE repo=? AND kind='operation'").run(String(repoId));
|
|
80
|
+
db.prepare(`INSERT INTO search_index(kind,name,path,repo)
|
|
81
|
+
SELECT 'operation',o.operation_name,o.operation_path,?
|
|
82
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
|
|
83
|
+
WHERE s.repo_id=? ORDER BY o.operation_name,o.operation_path,o.id`).run(String(repoId), repoId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function markRepositoryDerivedFactsChanged(db: Db, repoId: number): void {
|
|
87
|
+
db.prepare("UPDATE repositories SET fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='derived_facts_changed', graph_stale_at=? WHERE id=?").run(new Date().toISOString(), repoId);
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
function resolveBase(db: Db, workspaceId: number, extension: ExtensionRow): BaseRow[] {
|
|
@@ -111,6 +111,7 @@ function nodeContains(parent: ts.Node, child: ts.Node): boolean {
|
|
|
111
111
|
}
|
|
112
112
|
function declarationScope(node: ts.VariableDeclaration | ts.ParameterDeclaration): ts.Node {
|
|
113
113
|
if (ts.isParameter(node)) return node.parent;
|
|
114
|
+
if (ts.isCatchClause(node.parent) && node.parent.variableDeclaration === node) return node.parent;
|
|
114
115
|
const list = node.parent;
|
|
115
116
|
const blockScoped = (list.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0;
|
|
116
117
|
let current: ts.Node = list.parent;
|
|
@@ -118,26 +119,23 @@ function declarationScope(node: ts.VariableDeclaration | ts.ParameterDeclaration
|
|
|
118
119
|
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
119
120
|
return current;
|
|
120
121
|
}
|
|
121
|
-
while (current.parent && !ts.isBlock(current) && !ts.isSourceFile(current) && !ts.isModuleBlock(current) && !ts.isCaseBlock(current) && !
|
|
122
|
+
while (current.parent && !ts.isBlock(current) && !ts.isSourceFile(current) && !ts.isModuleBlock(current) && !ts.isCaseBlock(current) && !isLoopInitializerScope(node, current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
122
123
|
return current;
|
|
123
124
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return undefined;
|
|
125
|
+
function isLoopInitializerScope(declaration: ts.VariableDeclaration, scope: ts.Node): boolean {
|
|
126
|
+
const list = declaration.parent;
|
|
127
|
+
return (ts.isForStatement(scope) && scope.initializer === list) || ((ts.isForInStatement(scope) || ts.isForOfStatement(scope)) && scope.initializer === list);
|
|
128
|
+
}
|
|
129
|
+
function catchBindingScope(declaration: ts.VariableDeclaration | ts.ParameterDeclaration): ts.CatchClause | undefined {
|
|
130
|
+
if (ts.isParameter(declaration)) return undefined;
|
|
131
|
+
return ts.isCatchClause(declaration.parent) && declaration.parent.variableDeclaration === declaration ? declaration.parent : undefined;
|
|
132
132
|
}
|
|
133
133
|
function isAccessibleDeclaration(declaration: ts.VariableDeclaration | ts.ParameterDeclaration, use: ts.Node): boolean {
|
|
134
134
|
const source = use.getSourceFile();
|
|
135
135
|
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
136
|
-
const
|
|
137
|
-
if (
|
|
138
|
-
if (scopedAncestor && (ts.isForStatement(scopedAncestor) || ts.isForInStatement(scopedAncestor) || ts.isForOfStatement(scopedAncestor))) return nodeContains(scopedAncestor.statement, use);
|
|
136
|
+
const catchScope = catchBindingScope(declaration);
|
|
137
|
+
if (catchScope) return nodeContains(catchScope.block, use);
|
|
139
138
|
const scope = declarationScope(declaration);
|
|
140
|
-
if (ts.isCatchClause(scope)) return nodeContains(scope.block, use);
|
|
141
139
|
if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
142
140
|
return ts.isSourceFile(scope) || nodeContains(scope, use);
|
|
143
141
|
}
|
|
@@ -247,6 +245,11 @@ function httpMethodFromObject(object: ts.ObjectLiteralExpression, use: ts.Node):
|
|
|
247
245
|
const text = resolveExpression(propertyInitializer(object, 'method'), use, 'literal').value;
|
|
248
246
|
return text ? stripQuotes(text).toUpperCase() : undefined;
|
|
249
247
|
}
|
|
248
|
+
const supportedHttpMethods = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']);
|
|
249
|
+
function safeOperationName(value: string | undefined): string | undefined {
|
|
250
|
+
if (!value || !/^[A-Za-z_$][\w$]*(?:[./][A-Za-z_$][\w$]*)*$/.test(value)) return undefined;
|
|
251
|
+
return operationPathFromStatic(value);
|
|
252
|
+
}
|
|
250
253
|
function hasTemplatePlaceholder(value: string): boolean { return /\$\{|%7B|%7D/i.test(value); }
|
|
251
254
|
function urlTargetFromExpression(expr: ts.Expression | undefined, use: ts.Node): Record<string, unknown> {
|
|
252
255
|
const resolved = resolveExpression(expr, use, 'external');
|
|
@@ -426,16 +429,18 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
426
429
|
} else {
|
|
427
430
|
const receiver = receiverName(expr.expression);
|
|
428
431
|
const rootReceiver = rootReceiverName(expr.expression);
|
|
429
|
-
const
|
|
432
|
+
const firstArg = resolveExpression(node.arguments[0], node, 'literal');
|
|
433
|
+
const method = firstArg.value?.toUpperCase();
|
|
430
434
|
const pathArg = node.arguments[1];
|
|
431
|
-
const supported = method &&
|
|
435
|
+
const supported = method && supportedHttpMethods.has(method);
|
|
432
436
|
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
433
437
|
const resolvedPath = staticPathExpression(pathArg, node);
|
|
434
438
|
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
435
439
|
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
436
440
|
add(node, { callType: 'remote_action', serviceVariableName: rootReceiver ?? receiver, method, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.8 : 0.45, unresolvedReason: operationPathExpr ? undefined : 'dynamic_operation_path_identifier' }, { receiver, rootReceiver, classifier: 'service_client_send_method_path', rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? (resolvedPath.sourceKind === 'const' ? 'same_scope_const_initializer' : resolvedPath.sourceKind) : undefined, odataPathIntent: operationPathExpr ? intent : undefined, parserWarning: operationPathExpr ? undefined : 'dynamic_operation_path_identifier' });
|
|
437
441
|
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
438
|
-
|
|
442
|
+
const operationPathExpr = safeOperationName(firstArg.value);
|
|
443
|
+
add(node, { callType: 'remote_action', serviceVariableName: rootReceiver ?? receiver, operationPathExpr, payloadSummary: summarizeExpression(node.getText(source)), confidence: operationPathExpr ? 0.75 : 0.35, unresolvedReason: operationPathExpr ? undefined : 'unsupported_cap_send_signature' }, { receiver, rootReceiver, classifier: operationPathExpr ? 'service_client_send_operation_event' : 'service_client_send_unsupported_signature', rawOperationExpression: firstArg.rawExpression, literalOperationSource: firstArg.value ? firstArg.sourceKind : undefined, parserWarning: operationPathExpr ? undefined : 'unsupported_cap_send_signature' });
|
|
439
444
|
}
|
|
440
445
|
}
|
|
441
446
|
} else if (((ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) || (ts.isIdentifier(expr) && wrapperSpecs.has(expr.text)))) {
|