@saptools/service-flow 0.1.54 โ 0.1.55
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 +5 -0
- package/README.md +10 -5
- package/TECHNICAL-NOTE.md +6 -0
- package/dist/{chunk-ERIZHM5C.js โ chunk-GXYVIHJ5.js} +101 -10
- package/dist/chunk-GXYVIHJ5.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/outbound-call-parser.ts +116 -11
- package/dist/chunk-ERIZHM5C.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.55
|
|
4
|
+
|
|
5
|
+
- Indexed directly awaited CAP query-builder statements as local database calls, preserving exact builder-root evidence, fluent statement offsets, dynamic-entity warnings, method ownership, and the existing single `cds.run(...)` fact.
|
|
6
|
+
- Made normal CLI stdout pipeline-safe: an early downstream pipe close stops further output without an unhandled `EPIPE` stack trace, while unrelated stream failures remain non-zero diagnostics.
|
|
7
|
+
|
|
3
8
|
## 0.1.54
|
|
4
9
|
|
|
5
10
|
- Made contextual dynamic trace diagnostics runtime-current after `--var` substitution while retaining labelled pre-substitution binding state and distinct structural blockers.
|
package/README.md
CHANGED
|
@@ -156,6 +156,10 @@ service-flow link --workspace /path/to/workspace --force
|
|
|
156
156
|
| `--workspace <path>` | Workspace to link |
|
|
157
157
|
| `--force` | Accepted for workflow symmetry; linking always rebuilds graph edges |
|
|
158
158
|
|
|
159
|
+
### Pipeline-safe output
|
|
160
|
+
|
|
161
|
+
Normal command output is safe to pipe to a Unix consumer that intentionally stops reading early. A closed stdout pipe stops further output without an unhandled `EPIPE` stack trace; unrelated stdout failures still use the normal stderr diagnostic and non-zero exit outcome. Complete JSON, table, and Mermaid output bytes are unchanged.
|
|
162
|
+
|
|
159
163
|
### ๐งต `service-flow trace`
|
|
160
164
|
|
|
161
165
|
Trace one starting point and render table, JSON, or Mermaid output. Trace now
|
|
@@ -170,7 +174,7 @@ Local CAP calls through `cds.services.<Service>.<operation>()`, bracket service
|
|
|
170
174
|
|
|
171
175
|
Conservative local symbol traversal intentionally excludes decorators, built-ins such as `JSON.parse`, collection methods, third-party APIs, and arbitrary property chains unless the callee can plausibly resolve to an indexed local symbol. Named export lists such as `export { loadTemplate as publicLoadTemplate }` are indexed with the public exported name so relative imports can resolve. One-level object-literal helpers are indexed as symbols named like `cacheHelper.getConfiguration`; nested object literals are not yet expanded beyond the first helper level. `parseGeneratedConstants` remains a public low-level parser export for callers that need it, but generated constants are not persisted as graph facts in this patch; linking uses the deterministic decorator normalizer described above.
|
|
172
176
|
JSON output includes typed nodes for calls, operations, database entities,
|
|
173
|
-
external destinations, and unresolved/dynamic candidates when edges exist. Chained CAP DB queries inside `cds.run(...)` are parsed with TypeScript AST evidence for `SELECT`, `INSERT`, `UPDATE`, and `DELETE` forms. When the query target is genuinely dynamic, graph status remains terminal and JSON retains `parserWarning` evidence, while table and Mermaid render the target as `Entity: unknown` rather than a numeric call id.
|
|
177
|
+
external destinations, and unresolved/dynamic candidates when edges exist. Chained CAP DB queries inside `cds.run(...)` and directly awaited supported builders are parsed with TypeScript AST evidence for `SELECT`, `INSERT`, `UPSERT`, `UPDATE`, and `DELETE` forms. Direct builders must have a recognized CAP root; unrelated methods named `from`, `where`, or `set` are not promoted to database facts. When the query target is genuinely dynamic, graph status remains terminal and JSON retains `parserWarning` evidence, while table and Mermaid render the target as `Entity: unknown` rather than a numeric call id.
|
|
174
178
|
|
|
175
179
|
```bash
|
|
176
180
|
service-flow trace --workspace /path/to/workspace --repo facade-service --operation doWork
|
|
@@ -270,9 +274,10 @@ External HTTP facts use semantic terminal nodes instead of outbound-call row ids
|
|
|
270
274
|
a concrete client. Trace evidence includes the caller variable, returned
|
|
271
275
|
property, imported helper, source file, exported symbol, placeholders, and
|
|
272
276
|
transaction alias steps.
|
|
273
|
-
- `SELECT.one.from(Entity)`, `SELECT.from(Entity)`,
|
|
274
|
-
`
|
|
275
|
-
|
|
277
|
+
- Direct awaited `SELECT.one.from(Entity)`, `SELECT.from(Entity)`,
|
|
278
|
+
`INSERT.into(Entity)`, `UPSERT.into(Entity)`, `UPDATE(Entity)`, and
|
|
279
|
+
`DELETE.from(Entity)` statements, plus equivalent `cds.run(...)` queries,
|
|
280
|
+
are indexed as local database query entities when statically knowable.
|
|
276
281
|
- `doctor` reports silent quality problems such as services without operations,
|
|
277
282
|
handler repositories without CDS service facts, and an empty search index.
|
|
278
283
|
|
|
@@ -336,7 +341,7 @@ service-flow clean --workspace /path/to/workspace
|
|
|
336
341
|
| Handler facts | `cds-routing-handlers` decorators, handler classes/methods, server registrations |
|
|
337
342
|
| Service bindings | `cds.connect.to("alias")`, aliases from `package.json#cds.requires`, destination/service path expressions |
|
|
338
343
|
| Outbound calls | `remote.send({ method, path })`, `remote.send({ query })`, `cds.services.Service.operation()`, service wrapper calls |
|
|
339
|
-
| Local data access | `cds.run(SELECT...)
|
|
344
|
+
| Local data access | direct awaited CAP query builders, `cds.run(SELECT...)`, and local entity query evidence |
|
|
340
345
|
| Async channels | Event Mesh-style `emit`, `publish`, and `on` facts |
|
|
341
346
|
| External calls | Cloud SDK-style HTTP/destination calls and external edge evidence |
|
|
342
347
|
| Generated constants | low-level `parseGeneratedConstants` parser output for integrations; not persisted as first-class graph facts in this patch |
|
package/TECHNICAL-NOTE.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Service Flow Resolution Notes
|
|
2
2
|
|
|
3
|
+
## 0.1.55 direct query-builder and stdout notes
|
|
4
|
+
|
|
5
|
+
- Direct, awaited CAP builders now create one `local_db_query` fact when their AST root is a supported `SELECT`, `INSERT`, `UPSERT`, `UPDATE`, or `DELETE` form. The parser follows fluent continuations and transparent TypeScript wrappers, records the direct dispatch marker plus root and statement offsets, and keeps ordinary member-name lookalikes out of database facts. A builder nested in `cds.run(...)` remains one wrapper-dispatched fact.
|
|
6
|
+
- Static entities retain high confidence. Dynamic entity expressions retain the existing terminal unknown-entity behavior and parser warning; neither form evaluates application expressions. Fact insertion continues to use the logical statement location, so the narrowest executable symbol owns lifecycle and operation-method queries independently.
|
|
7
|
+
- CLI command output uses one shared stdout policy. `EPIPE` from an early-closing Unix consumer marks output complete and suppresses later writes. Other stdout errors are reported through the existing failure path, so valid full JSON, table, and Mermaid bytes are unchanged and genuine failures still exit non-zero.
|
|
8
|
+
|
|
3
9
|
## 0.1.54 runtime-current diagnostics and selected handler notes
|
|
4
10
|
|
|
5
11
|
- Contextual service-binding resolution now emits typed local trace state instead of passing a message string as control flow. `dynamic_missing`, `ambiguous_binding`, `ambiguous_operation`, `no_matching_operation`, and other conservative blockers remain distinguishable; `contextualPreSubstitutionState` preserves the historical attempt without changing a persisted graph row.
|
|
@@ -3045,19 +3045,105 @@ function variableInitializers(source) {
|
|
|
3045
3045
|
}
|
|
3046
3046
|
return initializers;
|
|
3047
3047
|
}
|
|
3048
|
+
function unwrapQueryExpression(expr) {
|
|
3049
|
+
if (ts8.isParenthesizedExpression(expr) || ts8.isAwaitExpression(expr) || ts8.isAsExpression(expr) || ts8.isTypeAssertionExpression(expr) || ts8.isNonNullExpression(expr) || ts8.isSatisfiesExpression(expr))
|
|
3050
|
+
return unwrapQueryExpression(expr.expression);
|
|
3051
|
+
return expr;
|
|
3052
|
+
}
|
|
3048
3053
|
function queryEntityFromAst(expr, initializers = /* @__PURE__ */ new Map()) {
|
|
3049
|
-
|
|
3050
|
-
if (ts8.isIdentifier(
|
|
3051
|
-
if (ts8.isCallExpression(
|
|
3052
|
-
const name = expressionName(
|
|
3053
|
-
if (name === "cds.run") return queryEntityFromAst(
|
|
3054
|
-
if (
|
|
3055
|
-
|
|
3056
|
-
const receiver = ts8.isPropertyAccessExpression(expr.expression) ? expr.expression.expression : void 0;
|
|
3054
|
+
const unwrapped = unwrapQueryExpression(expr);
|
|
3055
|
+
if (ts8.isIdentifier(unwrapped) && initializers.has(unwrapped.text)) return queryEntityFromAst(initializers.get(unwrapped.text), initializers);
|
|
3056
|
+
if (ts8.isCallExpression(unwrapped)) {
|
|
3057
|
+
const name = expressionName(unwrapped.expression);
|
|
3058
|
+
if (name === "cds.run") return queryEntityFromAst(unwrapped.arguments[0], initializers);
|
|
3059
|
+
if (capQueryBuilderRoots.has(name)) return entityFromExpression(unwrapped.arguments[0]);
|
|
3060
|
+
const receiver = ts8.isPropertyAccessExpression(unwrapped.expression) ? unwrapped.expression.expression : void 0;
|
|
3057
3061
|
if (receiver) return queryEntityFromAst(receiver, initializers);
|
|
3058
3062
|
}
|
|
3059
3063
|
return void 0;
|
|
3060
3064
|
}
|
|
3065
|
+
var capQueryBuilderRoots = /* @__PURE__ */ new Set([
|
|
3066
|
+
"SELECT.from",
|
|
3067
|
+
"SELECT.one.from",
|
|
3068
|
+
"SELECT.one",
|
|
3069
|
+
"INSERT.into",
|
|
3070
|
+
"UPSERT.into",
|
|
3071
|
+
"UPDATE.entity",
|
|
3072
|
+
"UPDATE",
|
|
3073
|
+
"DELETE.from"
|
|
3074
|
+
]);
|
|
3075
|
+
function wrapperParent(node) {
|
|
3076
|
+
const parent = node.parent;
|
|
3077
|
+
if ((ts8.isParenthesizedExpression(parent) || ts8.isAsExpression(parent) || ts8.isTypeAssertionExpression(parent) || ts8.isNonNullExpression(parent) || ts8.isSatisfiesExpression(parent)) && parent.expression === node)
|
|
3078
|
+
return parent;
|
|
3079
|
+
return void 0;
|
|
3080
|
+
}
|
|
3081
|
+
function fluentCallParent(node) {
|
|
3082
|
+
const property = node.parent;
|
|
3083
|
+
if (!ts8.isPropertyAccessExpression(property) || property.expression !== node) return void 0;
|
|
3084
|
+
const call = property.parent;
|
|
3085
|
+
return ts8.isCallExpression(call) && call.expression === property ? call : void 0;
|
|
3086
|
+
}
|
|
3087
|
+
function queryBuilderRoot(expr) {
|
|
3088
|
+
const unwrapped = unwrapQueryExpression(expr);
|
|
3089
|
+
if (!ts8.isCallExpression(unwrapped)) return void 0;
|
|
3090
|
+
if (capQueryBuilderRoots.has(expressionName(unwrapped.expression))) return unwrapped;
|
|
3091
|
+
return ts8.isPropertyAccessExpression(unwrapped.expression) ? queryBuilderRoot(unwrapped.expression.expression) : void 0;
|
|
3092
|
+
}
|
|
3093
|
+
function outerFluentQueryCall(root) {
|
|
3094
|
+
let current = root;
|
|
3095
|
+
let outer = root;
|
|
3096
|
+
while (true) {
|
|
3097
|
+
const wrapper = wrapperParent(current);
|
|
3098
|
+
if (wrapper) {
|
|
3099
|
+
current = wrapper;
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
const next = fluentCallParent(current);
|
|
3103
|
+
if (!next) return outer;
|
|
3104
|
+
outer = next;
|
|
3105
|
+
current = next;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
function directQueryBuilderStatement(node) {
|
|
3109
|
+
const root = queryBuilderRoot(node);
|
|
3110
|
+
if (!root) return void 0;
|
|
3111
|
+
const logicalCall = outerFluentQueryCall(root);
|
|
3112
|
+
if (logicalCall !== node) return void 0;
|
|
3113
|
+
let current = logicalCall;
|
|
3114
|
+
while (true) {
|
|
3115
|
+
const wrapper = wrapperParent(current);
|
|
3116
|
+
if (wrapper) {
|
|
3117
|
+
current = wrapper;
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
const parent = current.parent;
|
|
3121
|
+
return ts8.isAwaitExpression(parent) && parent.expression === current ? { root, logicalCall, awaitExpression: parent } : void 0;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function queryBuilderEvidence(source, statement) {
|
|
3125
|
+
return {
|
|
3126
|
+
classifier: "cap_query_builder_direct",
|
|
3127
|
+
queryDispatch: "direct_query_builder",
|
|
3128
|
+
queryRoot: expressionName(statement.root.expression),
|
|
3129
|
+
queryRootStartOffset: statement.root.getStart(source),
|
|
3130
|
+
queryRootEndOffset: statement.root.getEnd(),
|
|
3131
|
+
queryStatementStartOffset: statement.awaitExpression.getStart(source),
|
|
3132
|
+
queryStatementEndOffset: statement.awaitExpression.getEnd()
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
function queryRunEvidence(source, argument) {
|
|
3136
|
+
const root = argument ? queryBuilderRoot(argument) : void 0;
|
|
3137
|
+
return {
|
|
3138
|
+
classifier: "cap_query_run_wrapper",
|
|
3139
|
+
queryDispatch: "cds_run_wrapper",
|
|
3140
|
+
...root ? {
|
|
3141
|
+
queryRoot: expressionName(root.expression),
|
|
3142
|
+
queryRootStartOffset: root.getStart(source),
|
|
3143
|
+
queryRootEndOffset: root.getEnd()
|
|
3144
|
+
} : {}
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3061
3147
|
function extractQueryEntity(expr) {
|
|
3062
3148
|
const source = ts8.createSourceFile("query.ts", `const __query = (${expr});`, ts8.ScriptTarget.Latest, true, ts8.ScriptKind.TS);
|
|
3063
3149
|
const initializers = variableInitializers(source);
|
|
@@ -3410,11 +3496,16 @@ function classifyOutboundCallsInSource(source, filePath) {
|
|
|
3410
3496
|
}
|
|
3411
3497
|
const expr = node.expression;
|
|
3412
3498
|
const exprText = expr.getText(source);
|
|
3499
|
+
const directQuery = directQueryBuilderStatement(node);
|
|
3413
3500
|
if (exprText === "cds.run") {
|
|
3414
3501
|
const arg = node.arguments[0];
|
|
3415
3502
|
const entity = arg ? queryEntityFromAst(arg, initializers) : void 0;
|
|
3416
3503
|
const payload = arg?.getText(source) ?? "";
|
|
3417
|
-
add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) });
|
|
3504
|
+
add(node, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) }, queryRunEvidence(source, arg));
|
|
3505
|
+
} else if (directQuery) {
|
|
3506
|
+
const entity = queryEntityFromAst(directQuery.logicalCall, initializers);
|
|
3507
|
+
const payload = directQuery.logicalCall.getText(source);
|
|
3508
|
+
add(directQuery.logicalCall, { callType: "local_db_query", queryEntity: entity, payloadSummary: summarizeExpression(payload), confidence: entity ? 0.9 : 0.55, unresolvedReason: entity ? void 0 : queryWarning(payload) }, queryBuilderEvidence(source, directQuery));
|
|
3418
3509
|
} else if (ts8.isPropertyAccessExpression(expr) && expr.name.text === "send" && (ts8.isIdentifier(expr.expression) || ts8.isPropertyAccessExpression(expr.expression))) {
|
|
3419
3510
|
const objectArg = node.arguments[0];
|
|
3420
3511
|
if (objectArg && ts8.isObjectLiteralExpression(objectArg)) {
|
|
@@ -8250,4 +8341,4 @@ export {
|
|
|
8250
8341
|
selectorRepoAmbiguousDiagnostic,
|
|
8251
8342
|
trace
|
|
8252
8343
|
};
|
|
8253
|
-
//# sourceMappingURL=chunk-
|
|
8344
|
+
//# sourceMappingURL=chunk-GXYVIHJ5.js.map
|