@saptools/service-flow 0.1.54 → 0.1.56
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 +10 -0
- package/README.md +13 -5
- package/TECHNICAL-NOTE.md +12 -0
- package/dist/{chunk-ERIZHM5C.js → chunk-Y7H7ZU5B.js} +352 -113
- package/dist/chunk-Y7H7ZU5B.js.map +1 -0
- package/dist/cli.js +60 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/cli.ts +24 -16
- package/src/output/000-stdout-policy.ts +65 -0
- package/src/parsers/000-direct-query-execution.ts +287 -0
- package/src/parsers/outbound-call-parser.ts +53 -11
- package/dist/chunk-ERIZHM5C.js.map +0 -1
package/dist/index.js
CHANGED
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -30,9 +30,17 @@ import { renderTraceTable } from './output/table-output.js';
|
|
|
30
30
|
import { renderTraceJson, renderJson } from './output/json-output.js';
|
|
31
31
|
import { renderDoctorDiagnostics } from './output/doctor-output.js';
|
|
32
32
|
import { renderMermaid } from './output/mermaid-output.js';
|
|
33
|
+
import { createStdoutWriter } from './output/000-stdout-policy.js';
|
|
33
34
|
import { VERSION } from './version.js';
|
|
34
35
|
import type { DynamicMode } from './types.js';
|
|
35
36
|
import { cleanWorkspaceState } from './cli/000-clean.js';
|
|
37
|
+
|
|
38
|
+
const stdout = createStdoutWriter(process.stdout, fail);
|
|
39
|
+
|
|
40
|
+
function writeStdout(value: string): void {
|
|
41
|
+
stdout.write(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
async function init(
|
|
37
45
|
workspace: string,
|
|
38
46
|
options: { db?: string; ignore?: string[] },
|
|
@@ -58,7 +66,7 @@ async function init(
|
|
|
58
66
|
});
|
|
59
67
|
}
|
|
60
68
|
db.close();
|
|
61
|
-
|
|
69
|
+
writeStdout(
|
|
62
70
|
`Workspace: ${config.rootPath}\nDatabase: ${config.dbPath}\nRepositories: ${repos.length}\nIgnored: ${config.ignore.join(', ')}\nNext: service-flow index --workspace ${config.rootPath}\n`,
|
|
63
71
|
);
|
|
64
72
|
}
|
|
@@ -148,7 +156,7 @@ export function createProgram(): Command {
|
|
|
148
156
|
repo: opts.repo,
|
|
149
157
|
force: Boolean(opts.force),
|
|
150
158
|
});
|
|
151
|
-
|
|
159
|
+
writeStdout(
|
|
152
160
|
`Indexed ${r.indexedCount} repositories, skipped ${r.skippedCount}, ${r.fileCount} files, ${r.diagnosticCount} diagnostics\n`,
|
|
153
161
|
);
|
|
154
162
|
}).catch(fail),
|
|
@@ -162,7 +170,7 @@ export function createProgram(): Command {
|
|
|
162
170
|
void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
163
171
|
const r = linkWorkspace(db, workspaceId);
|
|
164
172
|
const upgradeWarnings = linkUpgradeWarnings(db);
|
|
165
|
-
|
|
173
|
+
writeStdout(
|
|
166
174
|
`${upgradeWarnings.length ? `Warnings: ${upgradeWarnings.map((item) => String(item.code)).join(', ')}. Run service-flow doctor --strict for remediation.\n` : ''}Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved\n`,
|
|
167
175
|
);
|
|
168
176
|
}).catch(fail),
|
|
@@ -227,7 +235,7 @@ export function createProgram(): Command {
|
|
|
227
235
|
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5),
|
|
228
236
|
},
|
|
229
237
|
);
|
|
230
|
-
|
|
238
|
+
writeStdout(
|
|
231
239
|
opts.format === 'json'
|
|
232
240
|
? renderTraceJson(result)
|
|
233
241
|
: opts.format === 'mermaid'
|
|
@@ -243,7 +251,7 @@ export function createProgram(): Command {
|
|
|
243
251
|
.action(
|
|
244
252
|
(opts: { workspace?: string }) =>
|
|
245
253
|
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) =>
|
|
246
|
-
|
|
254
|
+
writeStdout(
|
|
247
255
|
renderJson(
|
|
248
256
|
listRepositories(db, workspaceId).map((r) => ({
|
|
249
257
|
name: r.name,
|
|
@@ -265,7 +273,7 @@ export function createProgram(): Command {
|
|
|
265
273
|
? selectRepository(db, opts.repo, workspaceId)
|
|
266
274
|
: {};
|
|
267
275
|
if (selection.diagnostic) {
|
|
268
|
-
|
|
276
|
+
writeStdout(renderJson([selection.diagnostic]));
|
|
269
277
|
return;
|
|
270
278
|
}
|
|
271
279
|
const repo = selection.repo;
|
|
@@ -274,7 +282,7 @@ export function createProgram(): Command {
|
|
|
274
282
|
'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',
|
|
275
283
|
)
|
|
276
284
|
.all(workspaceId, repo?.id, repo?.id);
|
|
277
|
-
|
|
285
|
+
writeStdout(renderJson(rows));
|
|
278
286
|
}).catch(fail),
|
|
279
287
|
);
|
|
280
288
|
list
|
|
@@ -289,7 +297,7 @@ export function createProgram(): Command {
|
|
|
289
297
|
? selectRepository(db, opts.repo, workspaceId)
|
|
290
298
|
: {};
|
|
291
299
|
if (selection.diagnostic) {
|
|
292
|
-
|
|
300
|
+
writeStdout(renderJson([selection.diagnostic]));
|
|
293
301
|
return;
|
|
294
302
|
}
|
|
295
303
|
const repo = selection.repo;
|
|
@@ -298,7 +306,7 @@ export function createProgram(): Command {
|
|
|
298
306
|
'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=?)',
|
|
299
307
|
)
|
|
300
308
|
.all(workspaceId, repo?.id, repo?.id, opts.service, opts.service);
|
|
301
|
-
|
|
309
|
+
writeStdout(renderJson(rows));
|
|
302
310
|
}).catch(fail),
|
|
303
311
|
);
|
|
304
312
|
list
|
|
@@ -313,7 +321,7 @@ export function createProgram(): Command {
|
|
|
313
321
|
? selectRepository(db, opts.repo, workspaceId)
|
|
314
322
|
: {};
|
|
315
323
|
if (selection.diagnostic) {
|
|
316
|
-
|
|
324
|
+
writeStdout(renderJson([selection.diagnostic]));
|
|
317
325
|
return;
|
|
318
326
|
}
|
|
319
327
|
const repo = selection.repo;
|
|
@@ -330,7 +338,7 @@ export function createProgram(): Command {
|
|
|
330
338
|
opts.operation ? `/${opts.operation}` : undefined,
|
|
331
339
|
opts.operation ? `%${opts.operation}%` : undefined,
|
|
332
340
|
);
|
|
333
|
-
|
|
341
|
+
writeStdout(renderJson(rows));
|
|
334
342
|
}).catch(fail),
|
|
335
343
|
);
|
|
336
344
|
program
|
|
@@ -382,7 +390,7 @@ export function createProgram(): Command {
|
|
|
382
390
|
maxDynamicCandidates: parsePositiveInteger(opts.maxDynamicCandidates, 5),
|
|
383
391
|
},
|
|
384
392
|
);
|
|
385
|
-
|
|
393
|
+
writeStdout(
|
|
386
394
|
opts.format === 'json'
|
|
387
395
|
? renderTraceJson(result)
|
|
388
396
|
: renderMermaid(result),
|
|
@@ -398,7 +406,7 @@ export function createProgram(): Command {
|
|
|
398
406
|
(name: string, opts: { workspace?: string }) =>
|
|
399
407
|
void withReadOnlyWorkspace(opts.workspace, (db, workspaceId) => {
|
|
400
408
|
const selection = selectRepository(db, name, workspaceId);
|
|
401
|
-
|
|
409
|
+
writeStdout(renderJson(
|
|
402
410
|
selection.repo ?? selection.diagnostic ?? { error: 'repo not found' },
|
|
403
411
|
));
|
|
404
412
|
}).catch(fail),
|
|
@@ -415,7 +423,7 @@ export function createProgram(): Command {
|
|
|
415
423
|
'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=?)',
|
|
416
424
|
)
|
|
417
425
|
.all(workspaceId, selector, selector);
|
|
418
|
-
|
|
426
|
+
writeStdout(renderJson(rows));
|
|
419
427
|
}).catch(fail),
|
|
420
428
|
);
|
|
421
429
|
program
|
|
@@ -428,7 +436,7 @@ export function createProgram(): Command {
|
|
|
428
436
|
(opts: { workspace?: string; strict?: boolean; detail?: boolean; format?: string }) =>
|
|
429
437
|
void withReadOnlyWorkspace(opts.workspace, (db) => {
|
|
430
438
|
const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), { detail: Boolean(opts.detail) });
|
|
431
|
-
|
|
439
|
+
writeStdout(renderDoctorDiagnostics(allDiagnostics, opts.format));
|
|
432
440
|
}).catch(fail),
|
|
433
441
|
);
|
|
434
442
|
program
|
|
@@ -440,7 +448,7 @@ export function createProgram(): Command {
|
|
|
440
448
|
void (async () => {
|
|
441
449
|
const config = await loadWorkspaceConfig(opts.workspace);
|
|
442
450
|
await cleanWorkspaceState(config, Boolean(opts.dbOnly));
|
|
443
|
-
|
|
451
|
+
writeStdout('Cleaned service-flow state\n');
|
|
444
452
|
})().catch(fail),
|
|
445
453
|
);
|
|
446
454
|
return program;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
|
|
3
|
+
export interface StdoutWriter {
|
|
4
|
+
write(value: string): boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface OutputState {
|
|
8
|
+
blocked: boolean;
|
|
9
|
+
unexpectedReported: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface WriterEntry {
|
|
13
|
+
state: OutputState;
|
|
14
|
+
writer: StdoutWriter;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const writers = new WeakMap<Writable, WriterEntry>();
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isBrokenPipe(error: Error): boolean {
|
|
24
|
+
return isRecord(error) && error.code === 'EPIPE';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function asError(value: unknown): Error {
|
|
28
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleStreamError(
|
|
32
|
+
state: OutputState,
|
|
33
|
+
error: Error,
|
|
34
|
+
onUnexpectedError: (error: Error) => void,
|
|
35
|
+
): void {
|
|
36
|
+
state.blocked = true;
|
|
37
|
+
if (isBrokenPipe(error) || state.unexpectedReported) return;
|
|
38
|
+
state.unexpectedReported = true;
|
|
39
|
+
onUnexpectedError(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createStdoutWriter(
|
|
43
|
+
stream: Writable,
|
|
44
|
+
onUnexpectedError: (error: Error) => void,
|
|
45
|
+
): StdoutWriter {
|
|
46
|
+
const existing = writers.get(stream);
|
|
47
|
+
if (existing) return existing.writer;
|
|
48
|
+
const state: OutputState = { blocked: false, unexpectedReported: false };
|
|
49
|
+
const writer: StdoutWriter = {
|
|
50
|
+
write(value: string): boolean {
|
|
51
|
+
if (state.blocked) return false;
|
|
52
|
+
try {
|
|
53
|
+
stream.write(value);
|
|
54
|
+
return true;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
handleStreamError(state, asError(error), onUnexpectedError);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
stream.on('error', (error: Error) =>
|
|
62
|
+
handleStreamError(state, error, onUnexpectedError));
|
|
63
|
+
writers.set(stream, { state, writer });
|
|
64
|
+
return writer;
|
|
65
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
const capQueryBuilderRoots = new Set([
|
|
4
|
+
'SELECT.from',
|
|
5
|
+
'SELECT.one.from',
|
|
6
|
+
'SELECT.one',
|
|
7
|
+
'INSERT.into',
|
|
8
|
+
'UPSERT.into',
|
|
9
|
+
'UPDATE.entity',
|
|
10
|
+
'UPDATE',
|
|
11
|
+
'DELETE.from',
|
|
12
|
+
]);
|
|
13
|
+
const promiseValueShadowCache = new WeakMap<ts.SourceFile, boolean>();
|
|
14
|
+
|
|
15
|
+
export type DirectQueryExecutionContext =
|
|
16
|
+
| 'await'
|
|
17
|
+
| 'async_return'
|
|
18
|
+
| 'promise_return'
|
|
19
|
+
| 'promise_aggregate';
|
|
20
|
+
|
|
21
|
+
export interface DirectQueryBuilderStatement {
|
|
22
|
+
root: ts.CallExpression;
|
|
23
|
+
logicalCall: ts.CallExpression;
|
|
24
|
+
statement: ts.Expression;
|
|
25
|
+
executionContext: DirectQueryExecutionContext;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isCapQueryBuilderRootName(name: string): boolean {
|
|
29
|
+
return capQueryBuilderRoots.has(name);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function queryBuilderRoot(
|
|
33
|
+
expression: ts.Expression,
|
|
34
|
+
): ts.CallExpression | undefined {
|
|
35
|
+
const unwrapped = unwrapQueryExpression(expression);
|
|
36
|
+
if (!ts.isCallExpression(unwrapped)) return undefined;
|
|
37
|
+
if (isCapQueryBuilderRootName(expressionName(unwrapped.expression)))
|
|
38
|
+
return unwrapped;
|
|
39
|
+
return ts.isPropertyAccessExpression(unwrapped.expression)
|
|
40
|
+
? queryBuilderRoot(unwrapped.expression.expression)
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function directQueryBuilderStatement(
|
|
45
|
+
node: ts.CallExpression,
|
|
46
|
+
): DirectQueryBuilderStatement | undefined {
|
|
47
|
+
const root = queryBuilderRoot(node);
|
|
48
|
+
if (!root) return undefined;
|
|
49
|
+
const logicalCall = outerFluentQueryCall(root);
|
|
50
|
+
if (logicalCall !== node) return undefined;
|
|
51
|
+
const expression = outerTransparentExpression(logicalCall);
|
|
52
|
+
const awaitExpression = directAwaitExpression(expression);
|
|
53
|
+
if (awaitExpression)
|
|
54
|
+
return { root, logicalCall, statement: awaitExpression, executionContext: 'await' };
|
|
55
|
+
const returnContext = returnExecutionContext(expression);
|
|
56
|
+
if (returnContext)
|
|
57
|
+
return { root, logicalCall, statement: expression, executionContext: returnContext };
|
|
58
|
+
if (isAwaitedPromiseAllElement(expression))
|
|
59
|
+
return { root, logicalCall, statement: expression, executionContext: 'promise_aggregate' };
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function expressionName(expression: ts.Expression): string {
|
|
64
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
65
|
+
if (ts.isPropertyAccessExpression(expression))
|
|
66
|
+
return `${expressionName(expression.expression)}.${expression.name.text}`;
|
|
67
|
+
return expression.getText();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unwrapQueryExpression(expression: ts.Expression): ts.Expression {
|
|
71
|
+
if (ts.isParenthesizedExpression(expression) || ts.isAwaitExpression(expression)
|
|
72
|
+
|| ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)
|
|
73
|
+
|| ts.isNonNullExpression(expression) || ts.isSatisfiesExpression(expression))
|
|
74
|
+
return unwrapQueryExpression(expression.expression);
|
|
75
|
+
return expression;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function wrapperParent(node: ts.Expression): ts.Expression | undefined {
|
|
79
|
+
const parent = node.parent;
|
|
80
|
+
if ((ts.isParenthesizedExpression(parent) || ts.isAsExpression(parent)
|
|
81
|
+
|| ts.isTypeAssertionExpression(parent) || ts.isNonNullExpression(parent)
|
|
82
|
+
|| ts.isSatisfiesExpression(parent)) && parent.expression === node)
|
|
83
|
+
return parent;
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fluentCallParent(node: ts.Expression): ts.CallExpression | undefined {
|
|
88
|
+
const property = node.parent;
|
|
89
|
+
if (!ts.isPropertyAccessExpression(property) || property.expression !== node)
|
|
90
|
+
return undefined;
|
|
91
|
+
const call = property.parent;
|
|
92
|
+
return ts.isCallExpression(call) && call.expression === property ? call : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function outerFluentQueryCall(root: ts.CallExpression): ts.CallExpression {
|
|
96
|
+
let current: ts.Expression = root;
|
|
97
|
+
let outer = root;
|
|
98
|
+
while (true) {
|
|
99
|
+
const wrapper = wrapperParent(current);
|
|
100
|
+
if (wrapper) {
|
|
101
|
+
current = wrapper;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const next = fluentCallParent(current);
|
|
105
|
+
if (!next) return outer;
|
|
106
|
+
outer = next;
|
|
107
|
+
current = next;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function outerTransparentExpression(expression: ts.Expression): ts.Expression {
|
|
112
|
+
let current = expression;
|
|
113
|
+
while (true) {
|
|
114
|
+
const wrapper = wrapperParent(current);
|
|
115
|
+
if (!wrapper) return current;
|
|
116
|
+
current = wrapper;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function directAwaitExpression(
|
|
121
|
+
expression: ts.Expression,
|
|
122
|
+
): ts.AwaitExpression | undefined {
|
|
123
|
+
const parent = expression.parent;
|
|
124
|
+
return ts.isAwaitExpression(parent) && parent.expression === expression
|
|
125
|
+
? parent
|
|
126
|
+
: undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function returnExecutionContext(
|
|
130
|
+
expression: ts.Expression,
|
|
131
|
+
): DirectQueryExecutionContext | undefined {
|
|
132
|
+
const callable = returnedExpressionCallable(expression);
|
|
133
|
+
if (!callable) return undefined;
|
|
134
|
+
if (hasAsyncModifier(callable)) return 'async_return';
|
|
135
|
+
return hasGuaranteedPromiseReturn(callable) ? 'promise_return' : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function returnedExpressionCallable(
|
|
139
|
+
expression: ts.Expression,
|
|
140
|
+
): ts.FunctionLikeDeclaration | undefined {
|
|
141
|
+
const parent = expression.parent;
|
|
142
|
+
if (ts.isArrowFunction(parent) && parent.body === expression) return parent;
|
|
143
|
+
if (!ts.isReturnStatement(parent) || parent.expression !== expression)
|
|
144
|
+
return undefined;
|
|
145
|
+
return nearestCallable(parent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function nearestCallable(node: ts.Node): ts.FunctionLikeDeclaration | undefined {
|
|
149
|
+
let current = node.parent;
|
|
150
|
+
while (current) {
|
|
151
|
+
if (isRuntimeCallable(current)) return current;
|
|
152
|
+
current = current.parent;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isRuntimeCallable(node: ts.Node): node is ts.FunctionLikeDeclaration {
|
|
158
|
+
return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
159
|
+
|| ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
|
|
160
|
+
|| ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node)
|
|
161
|
+
|| ts.isSetAccessorDeclaration(node);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasAsyncModifier(node: ts.Node): boolean {
|
|
165
|
+
return !isGeneratorCallable(node) && ts.canHaveModifiers(node) && (ts.getModifiers(node)?.some(
|
|
166
|
+
(modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword,
|
|
167
|
+
) ?? false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isGeneratorCallable(node: ts.Node): boolean {
|
|
171
|
+
return (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
|
|
172
|
+
|| ts.isMethodDeclaration(node)) && Boolean(node.asteriskToken);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hasGuaranteedPromiseReturn(
|
|
176
|
+
callable: ts.FunctionLikeDeclaration,
|
|
177
|
+
): boolean {
|
|
178
|
+
const returnType = declaredReturnType(callable);
|
|
179
|
+
return Boolean(returnType && isGuaranteedPromiseType(returnType));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function declaredReturnType(
|
|
183
|
+
callable: ts.FunctionLikeDeclaration,
|
|
184
|
+
): ts.TypeNode | undefined {
|
|
185
|
+
if (ts.isFunctionDeclaration(callable) || ts.isFunctionExpression(callable)
|
|
186
|
+
|| ts.isArrowFunction(callable) || ts.isMethodDeclaration(callable))
|
|
187
|
+
return callable.type;
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isGuaranteedPromiseType(type: ts.TypeNode): boolean {
|
|
192
|
+
if (ts.isParenthesizedTypeNode(type))
|
|
193
|
+
return isGuaranteedPromiseType(type.type);
|
|
194
|
+
if (ts.isTypeReferenceNode(type))
|
|
195
|
+
return isStandardPromiseTypeName(type.typeName);
|
|
196
|
+
if (ts.isUnionTypeNode(type))
|
|
197
|
+
return type.types.length > 0 && type.types.every(isGuaranteedPromiseType);
|
|
198
|
+
if (ts.isIntersectionTypeNode(type))
|
|
199
|
+
return type.types.some(isGuaranteedPromiseType);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isStandardPromiseTypeName(name: ts.EntityName): boolean {
|
|
204
|
+
if (ts.isIdentifier(name))
|
|
205
|
+
return name.text === 'Promise' || name.text === 'PromiseLike';
|
|
206
|
+
return ts.isIdentifier(name.left)
|
|
207
|
+
&& (name.left.text === 'globalThis' || name.left.text === 'global')
|
|
208
|
+
&& (name.right.text === 'Promise' || name.right.text === 'PromiseLike');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isAwaitedPromiseAllElement(expression: ts.Expression): boolean {
|
|
212
|
+
const array = directArrayParent(expression);
|
|
213
|
+
if (!array) return false;
|
|
214
|
+
const aggregate = aggregateCallForArray(array);
|
|
215
|
+
return Boolean(aggregate && isBuiltInPromiseAll(aggregate)
|
|
216
|
+
&& directAwaitExpression(outerTransparentExpression(aggregate)));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function directArrayParent(
|
|
220
|
+
expression: ts.Expression,
|
|
221
|
+
): ts.ArrayLiteralExpression | undefined {
|
|
222
|
+
const parent = expression.parent;
|
|
223
|
+
return ts.isArrayLiteralExpression(parent) && parent.elements.some(
|
|
224
|
+
(element) => element === expression,
|
|
225
|
+
) ? parent : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function aggregateCallForArray(
|
|
229
|
+
array: ts.ArrayLiteralExpression,
|
|
230
|
+
): ts.CallExpression | undefined {
|
|
231
|
+
const argument = outerTransparentExpression(array);
|
|
232
|
+
const parent = argument.parent;
|
|
233
|
+
return ts.isCallExpression(parent) && parent.arguments.length === 1
|
|
234
|
+
&& parent.arguments[0] === argument ? parent : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function isBuiltInPromiseAll(call: ts.CallExpression): boolean {
|
|
238
|
+
return ts.isPropertyAccessExpression(call.expression)
|
|
239
|
+
&& ts.isIdentifier(call.expression.expression)
|
|
240
|
+
&& call.expression.expression.text === 'Promise'
|
|
241
|
+
&& call.expression.name.text === 'all'
|
|
242
|
+
&& !hasPromiseValueShadow(call.getSourceFile());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function hasPromiseValueShadow(source: ts.SourceFile): boolean {
|
|
246
|
+
const cached = promiseValueShadowCache.get(source);
|
|
247
|
+
if (cached !== undefined) return cached;
|
|
248
|
+
let shadowed = false;
|
|
249
|
+
const visit = (node: ts.Node): void => {
|
|
250
|
+
if (shadowed) return;
|
|
251
|
+
if (declaresPromiseValue(node)) {
|
|
252
|
+
shadowed = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
ts.forEachChild(node, visit);
|
|
256
|
+
};
|
|
257
|
+
visit(source);
|
|
258
|
+
promiseValueShadowCache.set(source, shadowed);
|
|
259
|
+
return shadowed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function declaresPromiseValue(node: ts.Node): boolean {
|
|
263
|
+
if (ts.isVariableDeclaration(node) || ts.isParameter(node))
|
|
264
|
+
return bindingNameIsPromise(node.name);
|
|
265
|
+
if (ts.isImportClause(node))
|
|
266
|
+
return !node.isTypeOnly && nodeIsPromise(node.name);
|
|
267
|
+
if (ts.isImportSpecifier(node))
|
|
268
|
+
return !node.isTypeOnly && nodeIsPromise(node.name);
|
|
269
|
+
if (ts.isNamespaceImport(node) || ts.isImportEqualsDeclaration(node))
|
|
270
|
+
return nodeIsPromise(node.name);
|
|
271
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)
|
|
272
|
+
|| ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node))
|
|
273
|
+
return nodeIsPromise(node.name);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function bindingNameIsPromise(name: ts.BindingName): boolean {
|
|
278
|
+
if (ts.isIdentifier(name)) return name.text === 'Promise';
|
|
279
|
+
if (ts.isObjectBindingPattern(name))
|
|
280
|
+
return name.elements.some((element) => bindingNameIsPromise(element.name));
|
|
281
|
+
return name.elements.some((element) => ts.isBindingElement(element)
|
|
282
|
+
&& bindingNameIsPromise(element.name));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function nodeIsPromise(name: ts.Node | undefined): boolean {
|
|
286
|
+
return Boolean(name && ts.isIdentifier(name) && name.text === 'Promise');
|
|
287
|
+
}
|
|
@@ -8,6 +8,12 @@ import { summarizeExpression } from '../utils/redaction.js';
|
|
|
8
8
|
import { classifyODataPathIntent } from '../linker/odata-path-normalizer.js';
|
|
9
9
|
import { parseServiceBindings } from './service-binding-parser.js';
|
|
10
10
|
import { parseImportedWrapperCalls } from './imported-wrapper-parser.js';
|
|
11
|
+
import {
|
|
12
|
+
directQueryBuilderStatement,
|
|
13
|
+
isCapQueryBuilderRootName,
|
|
14
|
+
queryBuilderRoot,
|
|
15
|
+
type DirectQueryBuilderStatement,
|
|
16
|
+
} from './000-direct-query-execution.js';
|
|
11
17
|
import type { RepositorySourceContext } from './ts-project.js';
|
|
12
18
|
import {
|
|
13
19
|
analyzeOperationPath,
|
|
@@ -40,19 +46,52 @@ function variableInitializers(source: ts.SourceFile): Map<string, ts.Expression>
|
|
|
40
46
|
}
|
|
41
47
|
return initializers;
|
|
42
48
|
}
|
|
49
|
+
function unwrapQueryExpression(expr: ts.Expression): ts.Expression {
|
|
50
|
+
if (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)
|
|
51
|
+
|| ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr)
|
|
52
|
+
|| ts.isNonNullExpression(expr) || ts.isSatisfiesExpression(expr))
|
|
53
|
+
return unwrapQueryExpression(expr.expression);
|
|
54
|
+
return expr;
|
|
55
|
+
}
|
|
43
56
|
function queryEntityFromAst(expr: ts.Expression, initializers = new Map<string, ts.Expression>()): string | undefined {
|
|
44
|
-
|
|
45
|
-
if (ts.isIdentifier(
|
|
46
|
-
if (ts.isCallExpression(
|
|
47
|
-
const name = expressionName(
|
|
48
|
-
if (name === 'cds.run') return queryEntityFromAst(
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
const receiver = ts.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : undefined;
|
|
57
|
+
const unwrapped = unwrapQueryExpression(expr);
|
|
58
|
+
if (ts.isIdentifier(unwrapped) && initializers.has(unwrapped.text)) return queryEntityFromAst(initializers.get(unwrapped.text) as ts.Expression, initializers);
|
|
59
|
+
if (ts.isCallExpression(unwrapped)) {
|
|
60
|
+
const name = expressionName(unwrapped.expression);
|
|
61
|
+
if (name === 'cds.run') return queryEntityFromAst(unwrapped.arguments[0], initializers);
|
|
62
|
+
if (isCapQueryBuilderRootName(name)) return entityFromExpression(unwrapped.arguments[0]);
|
|
63
|
+
const receiver = ts.isPropertyAccessExpression(unwrapped.expression) ? unwrapped.expression.expression : undefined;
|
|
52
64
|
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
53
65
|
}
|
|
54
66
|
return undefined;
|
|
55
67
|
}
|
|
68
|
+
function queryBuilderEvidence(source: ts.SourceFile, statement: DirectQueryBuilderStatement): Record<string, unknown> {
|
|
69
|
+
return {
|
|
70
|
+
classifier: 'cap_query_builder_direct',
|
|
71
|
+
queryDispatch: 'direct_query_builder',
|
|
72
|
+
queryRoot: expressionName(statement.root.expression),
|
|
73
|
+
queryRootStartOffset: statement.root.getStart(source),
|
|
74
|
+
queryRootEndOffset: statement.root.getEnd(),
|
|
75
|
+
queryStatementStartOffset: statement.statement.getStart(source),
|
|
76
|
+
queryStatementEndOffset: statement.statement.getEnd(),
|
|
77
|
+
queryExecutionContext: statement.executionContext,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function queryRunEvidence(
|
|
81
|
+
source: ts.SourceFile,
|
|
82
|
+
argument: ts.Expression | undefined,
|
|
83
|
+
): Record<string, unknown> {
|
|
84
|
+
const root = argument ? queryBuilderRoot(argument) : undefined;
|
|
85
|
+
return {
|
|
86
|
+
classifier: 'cap_query_run_wrapper',
|
|
87
|
+
queryDispatch: 'cds_run_wrapper',
|
|
88
|
+
...(root ? {
|
|
89
|
+
queryRoot: expressionName(root.expression),
|
|
90
|
+
queryRootStartOffset: root.getStart(source),
|
|
91
|
+
queryRootEndOffset: root.getEnd(),
|
|
92
|
+
} : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
56
95
|
function extractQueryEntity(expr: string): string | undefined {
|
|
57
96
|
const source = ts.createSourceFile('query.ts', `const __query = (${expr});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
58
97
|
const initializers = variableInitializers(source);
|
|
@@ -98,7 +137,6 @@ function nameOfProperty(name: ts.PropertyName): string | undefined {
|
|
|
98
137
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
99
138
|
return undefined;
|
|
100
139
|
}
|
|
101
|
-
|
|
102
140
|
type ExpressionStatus = 'static' | 'dynamic' | 'ambiguous' | 'unknown';
|
|
103
141
|
type ExpressionSourceKind = 'string_literal' | 'no_substitution_template' | 'template_with_substitutions' | 'const_alias' | 'conditional_candidates' | 'dynamic_expression';
|
|
104
142
|
interface ExpressionResolution { status: ExpressionStatus; sourceKind: ExpressionSourceKind; value?: string; rawExpression?: string; placeholderKeys: string[]; evidence: string[]; constName?: string }
|
|
@@ -303,7 +341,6 @@ function externalHttpEvidence(node: ts.CallExpression, source: ts.SourceFile): {
|
|
|
303
341
|
}
|
|
304
342
|
return undefined;
|
|
305
343
|
}
|
|
306
|
-
|
|
307
344
|
function collectServiceVariables(source: ts.SourceFile): Set<string> {
|
|
308
345
|
const vars = new Set<string>(['cds', 'messaging', 'messageClient', 'eventClient']);
|
|
309
346
|
const visit = (node: ts.Node): void => {
|
|
@@ -423,11 +460,16 @@ export function classifyOutboundCallsInSource(source: ts.SourceFile, filePath: s
|
|
|
423
460
|
}
|
|
424
461
|
const expr = node.expression;
|
|
425
462
|
const exprText = expr.getText(source);
|
|
463
|
+
const directQuery = directQueryBuilderStatement(node);
|
|
426
464
|
if (exprText === 'cds.run') {
|
|
427
465
|
const arg = node.arguments[0];
|
|
428
466
|
const entity = arg ? queryEntityFromAst(arg, initializers) : undefined;
|
|
429
467
|
const payload = arg?.getText(source) ?? '';
|
|
430
|
-
add(node, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) });
|
|
468
|
+
add(node, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) }, queryRunEvidence(source, arg));
|
|
469
|
+
} else if (directQuery) {
|
|
470
|
+
const entity = queryEntityFromAst(directQuery.logicalCall, initializers);
|
|
471
|
+
const payload = directQuery.logicalCall.getText(source);
|
|
472
|
+
add(directQuery.logicalCall, { callType: 'local_db_query', queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? undefined : queryWarning(payload) }, queryBuilderEvidence(source, directQuery));
|
|
431
473
|
} else if (ts.isPropertyAccessExpression(expr) && expr.name.text === 'send' && (ts.isIdentifier(expr.expression) || ts.isPropertyAccessExpression(expr.expression))) {
|
|
432
474
|
const objectArg = node.arguments[0];
|
|
433
475
|
if (objectArg && ts.isObjectLiteralExpression(objectArg)) {
|