@saptools/service-flow 0.1.41 → 0.1.42
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-7L4QKKGN.js → chunk-RP3BJ64F.js} +74 -5
- package/dist/chunk-RP3BJ64F.js.map +1 -0
- package/dist/cli.js +28 -6
- 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 +27 -4
- package/src/linker/cross-repo-linker.ts +12 -2
- package/src/parsers/cds-parser.ts +18 -1
- package/src/parsers/outbound-call-parser.ts +29 -1
- package/dist/chunk-7L4QKKGN.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -15,13 +15,36 @@ interface BaseRow { id: number; repoId: number }
|
|
|
15
15
|
export function materializeCdsExtensionOperations(db: Db, workspaceId: number): void {
|
|
16
16
|
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
17
|
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
|
-
const insert = 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)
|
|
19
|
-
SELECT ?,operation_type,operation_name,operation_path,params_json,return_type,source_file,source_line,'inherited',id FROM cds_operations WHERE service_id=? AND NOT EXISTS (SELECT 1 FROM cds_operations existing WHERE existing.service_id=? AND existing.operation_name=cds_operations.operation_name AND existing.operation_path=cds_operations.operation_path)`);
|
|
20
18
|
for (const extension of extensions) {
|
|
21
19
|
const bases = resolveBase(db, workspaceId, extension);
|
|
22
20
|
const status = bases.length === 1 ? 'resolved' : bases.length > 1 ? 'ambiguous' : 'unresolved';
|
|
23
|
-
db.prepare('UPDATE cds_services SET extension_base_status=?, extension_base_service_id=? WHERE id=?').run(status, bases[0]?.id
|
|
24
|
-
if (bases.length
|
|
21
|
+
db.prepare('UPDATE cds_services SET extension_base_status=?, extension_base_service_id=? WHERE id=?').run(status, status === 'resolved' ? bases[0]?.id : null, extension.id);
|
|
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;
|
|
26
|
+
}
|
|
27
|
+
reconcileInheritedOperations(db, extension, bases[0]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function reconcileInheritedOperations(db: Db, extension: ExtensionRow, base: BaseRow): void {
|
|
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 Array<{ id: number; operationName: string; operationPath: string; baseOperationId: number | null }>;
|
|
33
|
+
const desired = 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)").all(base.id, extension.id) as Array<Record<string, unknown>>;
|
|
34
|
+
const desiredKeys = new Set(desired.map((row) => `${String(row.operationName)}\0${String(row.operationPath)}`));
|
|
35
|
+
const byKey = new Map(existing.map((row) => [`${row.operationName}\0${row.operationPath}`, row]));
|
|
36
|
+
for (const row of existing) {
|
|
37
|
+
if (!desiredKeys.has(`${row.operationName}\0${row.operationPath}`)) db.prepare('DELETE FROM cds_operations WHERE id=?').run(row.id);
|
|
38
|
+
}
|
|
39
|
+
const update = db.prepare('UPDATE cds_operations SET operation_type=?,params_json=?,return_type=?,source_file=?,source_line=?,base_operation_id=? WHERE id=?');
|
|
40
|
+
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
|
+
for (const row of desired) {
|
|
43
|
+
const key = `${String(row.operationName)}\0${String(row.operationPath)}`;
|
|
44
|
+
const current = byKey.get(key);
|
|
45
|
+
if (current) update.run(row.operationType, row.paramsJson, row.returnType, row.sourceFile, row.sourceLine, row.id, current.id);
|
|
46
|
+
else add.run(extension.id, row.operationType, row.operationName, row.operationPath, row.paramsJson, row.returnType, row.sourceFile, row.sourceLine, row.id);
|
|
47
|
+
search.run('operation', row.operationName, row.operationPath, String(extension.repoId), 'operation', row.operationName, row.operationPath, String(extension.repoId));
|
|
25
48
|
}
|
|
26
49
|
}
|
|
27
50
|
|
|
@@ -150,13 +150,14 @@ function callEvidence(call: Record<string, unknown>, resolution: { target?: { re
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function linkImplementations(db: Db, workspaceId: number, generation: number): { edgeCount: number; resolvedCount: number; ambiguousCount: number; unresolvedCount: number } {
|
|
153
|
-
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind 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=?`).all(workspaceId) as Array<Record<string, unknown>>;
|
|
153
|
+
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind 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=?`).all(workspaceId) as Array<Record<string, unknown>>;
|
|
154
154
|
let edgeCount = 0;
|
|
155
155
|
let resolvedCount = 0;
|
|
156
156
|
let ambiguousCount = 0;
|
|
157
157
|
let unresolvedCount = 0;
|
|
158
158
|
for (const operation of operations) {
|
|
159
|
-
const
|
|
159
|
+
const implementationContext = implementationContextForOperation(db, operation);
|
|
160
|
+
const candidates = rankedImplementationCandidates(db, workspaceId, implementationContext);
|
|
160
161
|
if (candidates.length === 0) continue;
|
|
161
162
|
const accepted = candidates.filter((candidate) => candidate.accepted);
|
|
162
163
|
const topScore = accepted[0]?.score ?? 0;
|
|
@@ -167,6 +168,9 @@ function linkImplementations(db: Db, workspaceId: number, generation: number): {
|
|
|
167
168
|
operationPath: operation.operationPath,
|
|
168
169
|
operationName: operation.operationName,
|
|
169
170
|
modelPackage: { id: operation.modelRepoId, name: operation.modelRepo, packageName: operation.modelPackage },
|
|
171
|
+
implementationSource: implementationContext.operationId === operation.operationId ? 'direct_or_concrete_override' : 'inherited_from_base_operation',
|
|
172
|
+
baseOperationId: operation.baseOperationId,
|
|
173
|
+
implementationOperationId: implementationContext.operationId,
|
|
170
174
|
candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1)),
|
|
171
175
|
};
|
|
172
176
|
if (accepted.length === 0) {
|
|
@@ -190,6 +194,12 @@ interface ImplementationCandidate extends Record<string, unknown> {
|
|
|
190
194
|
acceptedReasons: string[];
|
|
191
195
|
rejectedReasons: string[];
|
|
192
196
|
}
|
|
197
|
+
function implementationContextForOperation(db: Db, operation: Record<string, unknown>): Record<string, unknown> {
|
|
198
|
+
if (operation.provenance !== 'inherited' || !operation.baseOperationId) return operation;
|
|
199
|
+
const base = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,o.provenance provenance,o.base_operation_id baseOperationId,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operation.baseOperationId) as Record<string, unknown> | undefined;
|
|
200
|
+
if (!base) return operation;
|
|
201
|
+
return { ...base, effectiveOperationId: operation.operationId, effectiveServicePath: operation.servicePath, effectiveOperationPath: operation.operationPath };
|
|
202
|
+
}
|
|
193
203
|
function rankedImplementationCandidates(db: Db, workspaceId: number, operation: Record<string, unknown>): ImplementationCandidate[] {
|
|
194
204
|
const rows = implementationCandidates(db, workspaceId, operation);
|
|
195
205
|
return deduplicateCandidates(rows.map((row) => scoreImplementationCandidate(row, operation))).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
|
|
@@ -45,6 +45,23 @@ function maskCommentsAndStrings(text: string): string {
|
|
|
45
45
|
return out;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function maskComments(text: string): string {
|
|
49
|
+
let out = '';
|
|
50
|
+
let mode: 'code' | 'line' | 'block' | 'single' | 'double' | 'template' = 'code';
|
|
51
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
52
|
+
const c = text[i] ?? '';
|
|
53
|
+
const n = text[i + 1] ?? '';
|
|
54
|
+
if (mode === 'code' && c === '/' && n === '/') { mode = 'line'; out += ' '; i += 1; continue; }
|
|
55
|
+
if (mode === 'code' && c === '/' && n === '*') { mode = 'block'; out += ' '; i += 1; continue; }
|
|
56
|
+
if (mode === 'line' && c === '\n') mode = 'code';
|
|
57
|
+
if (mode === 'block' && c === '*' && n === '/') { mode = 'code'; out += ' '; i += 1; continue; }
|
|
58
|
+
if (mode === 'code' && (c === "'" || c === '"' || c === '`')) mode = c === "'" ? 'single' : c === '"' ? 'double' : 'template';
|
|
59
|
+
else if ((mode === 'single' && c === "'") || (mode === 'double' && c === '"') || (mode === 'template' && c === '`')) mode = 'code';
|
|
60
|
+
out += mode === 'line' || mode === 'block' ? (c === '\n' ? '\n' : ' ') : c;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
function readAnnotation(text: string, index: number): { end: number; raw: string } | undefined {
|
|
49
66
|
if (text[index] !== '@') return undefined;
|
|
50
67
|
let i = index + 1;
|
|
@@ -126,7 +143,7 @@ export async function parseCdsFile(repoPath: string, filePath: string): Promise<
|
|
|
126
143
|
const namespace = /namespace\s+([\w.]+)\s*;/.exec(masked)?.[1];
|
|
127
144
|
const services: CdsServiceFact[] = [];
|
|
128
145
|
const pendingAnnotations: Array<{ end: number; raw: string }> = [];
|
|
129
|
-
const usings = collectUsings(text);
|
|
146
|
+
const usings = collectUsings(maskComments(text));
|
|
130
147
|
for (const a of masked.matchAll(/@\s*\(/g)) pendingAnnotations.push(annotationRawAt(text, masked, a.index ?? 0));
|
|
131
148
|
const serviceRegex = /\b(?:(extend)\s+)?(?:(service)\s+)?([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\b/g;
|
|
132
149
|
let match: RegExpExecArray | null;
|
|
@@ -118,13 +118,27 @@ function declarationScope(node: ts.VariableDeclaration | ts.ParameterDeclaration
|
|
|
118
118
|
while (current.parent && !isFunctionLikeScope(current)) current = current.parent;
|
|
119
119
|
return current;
|
|
120
120
|
}
|
|
121
|
-
while (current.parent && !ts.isBlock(current) && !ts.isSourceFile(current) && !ts.isModuleBlock(current) && !ts.isCaseBlock(current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
121
|
+
while (current.parent && !ts.isBlock(current) && !ts.isSourceFile(current) && !ts.isModuleBlock(current) && !ts.isCaseBlock(current) && !ts.isCatchClause(current) && !ts.isForStatement(current) && !ts.isForInStatement(current) && !ts.isForOfStatement(current) && !isFunctionLikeScope(current)) current = current.parent;
|
|
122
122
|
return current;
|
|
123
123
|
}
|
|
124
|
+
function declarationScopedAncestor(node: ts.Node): ts.Node | undefined {
|
|
125
|
+
let current: ts.Node | undefined = node.parent;
|
|
126
|
+
while (current) {
|
|
127
|
+
if (ts.isCatchClause(current) || ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) return current;
|
|
128
|
+
if (ts.isSourceFile(current) || ts.isFunctionLike(current)) return undefined;
|
|
129
|
+
current = current.parent;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
124
133
|
function isAccessibleDeclaration(declaration: ts.VariableDeclaration | ts.ParameterDeclaration, use: ts.Node): boolean {
|
|
125
134
|
const source = use.getSourceFile();
|
|
126
135
|
if (declaration.name.getStart(source) >= use.getStart(source)) return false;
|
|
136
|
+
const scopedAncestor = declarationScopedAncestor(declaration);
|
|
137
|
+
if (scopedAncestor && ts.isCatchClause(scopedAncestor)) return nodeContains(scopedAncestor.block, use);
|
|
138
|
+
if (scopedAncestor && (ts.isForStatement(scopedAncestor) || ts.isForInStatement(scopedAncestor) || ts.isForOfStatement(scopedAncestor))) return nodeContains(scopedAncestor.statement, use);
|
|
127
139
|
const scope = declarationScope(declaration);
|
|
140
|
+
if (ts.isCatchClause(scope)) return nodeContains(scope.block, use);
|
|
141
|
+
if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) return nodeContains(scope.statement, use);
|
|
128
142
|
return ts.isSourceFile(scope) || nodeContains(scope, use);
|
|
129
143
|
}
|
|
130
144
|
function resolveBinding(identifier: ts.Identifier, use: ts.Node): BindingResolution {
|
|
@@ -409,6 +423,20 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
409
423
|
const entityCallType = entityCallTypes[intent.kind];
|
|
410
424
|
const isODataQueryRead = method.toUpperCase() === 'GET' && ['entity_query', 'entity_key_read', 'entity_navigation_query'].includes(intent.kind);
|
|
411
425
|
add(node, { callType: query ? 'remote_query' : entityCallType ?? (isODataQueryRead ? 'remote_query' : 'remote_action'), serviceVariableName: receiver, method, operationPathExpr, queryEntity: query ? extractQueryEntity(query) : isODataQueryRead ? intent.entitySegment : undefined, payloadSummary: summarizeExpression(objectArg.getText(source)), confidence: op || query ? 0.8 : 0.4, unresolvedReason: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined }, { receiver, classifier: 'service_client_send_object', operationPathExpression: shorthandPath ? op : undefined, rawPathExpression: resolvedPath.rawExpression, literalPathSource: resolvedPath.text ? (resolvedPath.sourceKind === 'const' ? 'same_scope_const_initializer' : resolvedPath.sourceKind) : undefined, odataPathIntent: operationPathExpr ? intent : undefined, staticPathCandidates: candidateEvidence, parserWarning: !query && pathExpr && !operationPathExpr ? 'dynamic_operation_path_identifier' : undefined });
|
|
426
|
+
} else {
|
|
427
|
+
const receiver = receiverName(expr.expression);
|
|
428
|
+
const rootReceiver = rootReceiverName(expr.expression);
|
|
429
|
+
const method = resolveExpression(node.arguments[0], node, 'literal').value?.toUpperCase();
|
|
430
|
+
const pathArg = node.arguments[1];
|
|
431
|
+
const supported = method && ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].includes(method);
|
|
432
|
+
if (receiver && supported && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
433
|
+
const resolvedPath = staticPathExpression(pathArg, node);
|
|
434
|
+
const operationPathExpr = operationPathFromStatic(resolvedPath.text);
|
|
435
|
+
const intent = classifyODataPathIntent(operationPathExpr, method);
|
|
436
|
+
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
|
+
} else if (receiver && serviceVariables.has(rootReceiver ?? receiver)) {
|
|
438
|
+
add(node, { callType: 'remote_action', serviceVariableName: rootReceiver ?? receiver, method, payloadSummary: summarizeExpression(node.getText(source)), confidence: 0.35, unresolvedReason: 'unsupported_cap_send_signature' }, { receiver, rootReceiver, classifier: 'service_client_send_unsupported_signature' });
|
|
439
|
+
}
|
|
412
440
|
}
|
|
413
441
|
} else if (((ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) && wrapperSpecs.has(expr.expression.text)) || (ts.isIdentifier(expr) && wrapperSpecs.has(expr.text)))) {
|
|
414
442
|
const wrapperName = ts.isIdentifier(expr) ? expr.text : ts.isCallExpression(expr) && ts.isIdentifier(expr.expression) ? expr.expression.text : '';
|