@saptools/service-flow 0.1.51 → 0.1.53
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 +13 -0
- package/README.md +16 -14
- package/TECHNICAL-NOTE.md +18 -6
- package/dist/{chunk-YZJKE5UX.js → chunk-LFH7C46B.js} +4290 -1261
- package/dist/chunk-LFH7C46B.js.map +1 -0
- package/dist/cli.js +435 -515
- 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/001-doctor-projection.ts +140 -0
- package/src/cli/doctor.ts +20 -3
- 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 +130 -24
- 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/000-implementation-candidates.ts +641 -0
- package/src/linker/001-implementation-evidence-projection.ts +119 -0
- package/src/linker/002-call-evidence.ts +226 -0
- package/src/linker/cross-repo-linker.ts +27 -441
- package/src/linker/dynamic-edge-resolver.ts +35 -0
- package/src/linker/external-http-target.ts +24 -3
- package/src/linker/helper-package-linker.ts +18 -2
- package/src/linker/service-resolver.ts +45 -2
- package/src/output/doctor-output.ts +13 -4
- package/src/output/table-output.ts +33 -5
- 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 +96 -0
- package/src/trace/001-dynamic-identity.ts +308 -0
- package/src/trace/002-trace-diagnostics.ts +54 -0
- package/src/trace/003-dynamic-references.ts +283 -0
- package/src/trace/004-dynamic-candidate-sources.ts +155 -0
- package/src/trace/005-implementation-selection.ts +187 -0
- package/src/trace/006-contextual-projection.ts +30 -0
- package/src/trace/007-implementation-start-diagnostic.ts +61 -0
- package/src/trace/dynamic-targets.ts +582 -306
- package/src/trace/evidence.ts +331 -46
- package/src/trace/implementation-hints.ts +148 -8
- package/src/trace/selectors.ts +551 -3
- package/src/trace/trace-engine.ts +129 -135
- package/src/types.ts +27 -1
- package/src/utils/000-bounded-projection.ts +161 -0
- package/dist/chunk-YZJKE5UX.js.map +0 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import type {
|
|
3
|
+
DynamicTargetCandidate,
|
|
4
|
+
DynamicTemplates,
|
|
5
|
+
DynamicVariableProvenance,
|
|
6
|
+
} from './000-dynamic-target-types.js';
|
|
7
|
+
|
|
8
|
+
interface RouteImplementationEvidence {
|
|
9
|
+
routeRepoId?: number;
|
|
10
|
+
handlerRepo?: string;
|
|
11
|
+
operationProvenance?: string;
|
|
12
|
+
baseOperationId?: number;
|
|
13
|
+
edgeStatus?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface IdentityProposal {
|
|
17
|
+
operationId: number;
|
|
18
|
+
key: string;
|
|
19
|
+
value: string;
|
|
20
|
+
normalizedIdentity: string;
|
|
21
|
+
provenance: DynamicVariableProvenance;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface RepositoryIdentity {
|
|
25
|
+
repoId: number;
|
|
26
|
+
repoName: string;
|
|
27
|
+
packageName?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface WorkspaceIdentityMatch {
|
|
31
|
+
ownerKey: string;
|
|
32
|
+
key: string;
|
|
33
|
+
value: string;
|
|
34
|
+
normalizedIdentity: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IdentityDerivation {
|
|
38
|
+
operationId: number;
|
|
39
|
+
key: string;
|
|
40
|
+
value: string;
|
|
41
|
+
provenance: DynamicVariableProvenance;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function uniqueIdentityDerivations(
|
|
45
|
+
db: Db,
|
|
46
|
+
candidates: DynamicTargetCandidate[],
|
|
47
|
+
templates: DynamicTemplates,
|
|
48
|
+
): IdentityDerivation[] {
|
|
49
|
+
const identities = workspaceIdentities(db, candidates);
|
|
50
|
+
const proposals = candidates.flatMap((candidate) => {
|
|
51
|
+
const implementation = routeImplementationEvidence(
|
|
52
|
+
db, candidate.candidateOperationId,
|
|
53
|
+
);
|
|
54
|
+
const identity = identities.find((item) => item.repoId === candidate.repoId);
|
|
55
|
+
if (!identity || !implementation || !routeOwnerAgrees(candidate, implementation))
|
|
56
|
+
return [];
|
|
57
|
+
return identityProposals(candidate, identity, templates, implementation);
|
|
58
|
+
});
|
|
59
|
+
const matches = workspaceIdentityMatches(identities, templates);
|
|
60
|
+
const competing = competingIdentityKeys(matches);
|
|
61
|
+
const duplicates = duplicateNormalizedIdentities(identities);
|
|
62
|
+
return proposals
|
|
63
|
+
.filter((proposal) => !competing.has(`${proposal.key}:${proposal.value}`))
|
|
64
|
+
.filter((proposal) => !duplicates.has(proposal.normalizedIdentity))
|
|
65
|
+
.map((proposal) => ({
|
|
66
|
+
operationId: proposal.operationId,
|
|
67
|
+
key: proposal.key,
|
|
68
|
+
value: proposal.value,
|
|
69
|
+
provenance: proposal.provenance,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function identityProposals(
|
|
74
|
+
candidate: DynamicTargetCandidate,
|
|
75
|
+
identity: RepositoryIdentity,
|
|
76
|
+
templates: DynamicTemplates,
|
|
77
|
+
implementation: RouteImplementationEvidence,
|
|
78
|
+
): IdentityProposal[] {
|
|
79
|
+
const routeTemplates = [templates.alias, templates.destination]
|
|
80
|
+
.filter((value): value is string => Boolean(value));
|
|
81
|
+
const identities = [
|
|
82
|
+
{ name: identity.packageName, sourceKind: 'package_identity', npmPackage: true },
|
|
83
|
+
{ name: identity.repoName, sourceKind: 'repository_identity', npmPackage: false },
|
|
84
|
+
].filter((item): item is {
|
|
85
|
+
name: string; sourceKind: string; npmPackage: boolean;
|
|
86
|
+
} => Boolean(item.name));
|
|
87
|
+
const proposals = routeTemplates.flatMap((template) =>
|
|
88
|
+
identities.flatMap((identity) =>
|
|
89
|
+
proposalForIdentity(
|
|
90
|
+
candidate, template, identity.name, identity.sourceKind,
|
|
91
|
+
identity.npmPackage, implementation,
|
|
92
|
+
)));
|
|
93
|
+
return deduplicateProposals(proposals);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function proposalForIdentity(
|
|
97
|
+
candidate: DynamicTargetCandidate,
|
|
98
|
+
template: string,
|
|
99
|
+
identity: string,
|
|
100
|
+
sourceKind: string,
|
|
101
|
+
npmPackage: boolean,
|
|
102
|
+
implementation: RouteImplementationEvidence,
|
|
103
|
+
): IdentityProposal[] {
|
|
104
|
+
const match = matchIdentityTemplate(template, identity, npmPackage);
|
|
105
|
+
if (!match) return [];
|
|
106
|
+
return [{
|
|
107
|
+
operationId: candidate.candidateOperationId,
|
|
108
|
+
key: match.key,
|
|
109
|
+
value: match.value,
|
|
110
|
+
normalizedIdentity: match.normalizedIdentity,
|
|
111
|
+
provenance: {
|
|
112
|
+
sourceKind,
|
|
113
|
+
value: match.value,
|
|
114
|
+
rule: 'exact_normalized_identity_template_match',
|
|
115
|
+
template,
|
|
116
|
+
matchedName: identity,
|
|
117
|
+
normalizedForm: match.normalizedIdentity,
|
|
118
|
+
sourceRepo: candidate.repoName,
|
|
119
|
+
routeOwner: candidate.repoName,
|
|
120
|
+
candidateOperationId: candidate.candidateOperationId,
|
|
121
|
+
effectiveBaseOperationId: implementation.baseOperationId,
|
|
122
|
+
candidateOperationProvenance: implementation.operationProvenance,
|
|
123
|
+
implementationEdgeStatus: implementation.edgeStatus,
|
|
124
|
+
implementationHandlerRepo: implementation.handlerRepo,
|
|
125
|
+
},
|
|
126
|
+
}];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function matchIdentityTemplate(
|
|
130
|
+
template: string,
|
|
131
|
+
identity: string,
|
|
132
|
+
npmPackage: boolean,
|
|
133
|
+
): { key: string; value: string; normalizedIdentity: string } | undefined {
|
|
134
|
+
const matches = [...template.matchAll(/\$\{([^}]*)\}/g)];
|
|
135
|
+
if (matches.length !== 1 || !matches[0]?.[1]) return undefined;
|
|
136
|
+
const placeholder = matches[0][0];
|
|
137
|
+
const sentinel = 'dynamicplaceholdertoken';
|
|
138
|
+
const normalizedTemplate = normalizeIdentity(template.replace(placeholder, sentinel));
|
|
139
|
+
const [prefix, suffix, extra] = normalizedTemplate.split(sentinel);
|
|
140
|
+
if (!prefix || !suffix || extra !== undefined) return undefined;
|
|
141
|
+
const normalizedIdentity = normalizeIdentity(identity, npmPackage);
|
|
142
|
+
const match = new RegExp(`^${escapeRegex(prefix)}([a-z0-9]+)${escapeRegex(suffix)}$`)
|
|
143
|
+
.exec(normalizedIdentity);
|
|
144
|
+
if (!match?.[1]) return undefined;
|
|
145
|
+
return { key: matches[0][1].trim(), value: match[1], normalizedIdentity };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function competingIdentityKeys(
|
|
149
|
+
proposals: Array<{ key: string; value: string; ownerKey: string }>,
|
|
150
|
+
): Set<string> {
|
|
151
|
+
const owners = new Map<string, Set<string>>();
|
|
152
|
+
for (const proposal of proposals) {
|
|
153
|
+
const key = `${proposal.key}:${proposal.value}`;
|
|
154
|
+
owners.set(key, new Set([...(owners.get(key) ?? []), proposal.ownerKey]));
|
|
155
|
+
}
|
|
156
|
+
return new Set([...owners.entries()]
|
|
157
|
+
.filter(([, repos]) => repos.size > 1)
|
|
158
|
+
.map(([key]) => key));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function duplicateNormalizedIdentities(
|
|
162
|
+
identities: RepositoryIdentity[],
|
|
163
|
+
): Set<string> {
|
|
164
|
+
const owners = new Map<string, Set<string>>();
|
|
165
|
+
for (const identity of identities) {
|
|
166
|
+
for (const [name, npmPackage] of [
|
|
167
|
+
[identity.packageName, true],
|
|
168
|
+
[identity.repoName, false],
|
|
169
|
+
] as const) {
|
|
170
|
+
if (!name) continue;
|
|
171
|
+
const normalized = normalizeIdentity(name, npmPackage);
|
|
172
|
+
owners.set(normalized, new Set([
|
|
173
|
+
...(owners.get(normalized) ?? []),
|
|
174
|
+
`repository:${identity.repoId}`,
|
|
175
|
+
]));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return new Set([...owners.entries()]
|
|
179
|
+
.filter(([, repos]) => repos.size > 1)
|
|
180
|
+
.map(([identity]) => identity));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function workspaceIdentityMatches(
|
|
184
|
+
identities: RepositoryIdentity[],
|
|
185
|
+
templates: DynamicTemplates,
|
|
186
|
+
): WorkspaceIdentityMatch[] {
|
|
187
|
+
const routeTemplates = [templates.alias, templates.destination]
|
|
188
|
+
.filter((value): value is string => Boolean(value));
|
|
189
|
+
return identities.flatMap((identity) => {
|
|
190
|
+
const names: Array<{ name?: string; npmPackage: boolean }> = [
|
|
191
|
+
{ name: identity.packageName, npmPackage: true },
|
|
192
|
+
{ name: identity.repoName, npmPackage: false },
|
|
193
|
+
];
|
|
194
|
+
return routeTemplates.flatMap((template) =>
|
|
195
|
+
names.flatMap(({ name, npmPackage }) => {
|
|
196
|
+
if (!name) return [];
|
|
197
|
+
const match = matchIdentityTemplate(template, name, npmPackage);
|
|
198
|
+
return match ? [{
|
|
199
|
+
ownerKey: `repository:${identity.repoId}`,
|
|
200
|
+
key: match.key,
|
|
201
|
+
value: match.value,
|
|
202
|
+
normalizedIdentity: match.normalizedIdentity,
|
|
203
|
+
}] : [];
|
|
204
|
+
}));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function workspaceIdentities(
|
|
209
|
+
db: Db,
|
|
210
|
+
candidates: DynamicTargetCandidate[],
|
|
211
|
+
): RepositoryIdentity[] {
|
|
212
|
+
const repoIds = [...new Set(candidates.flatMap((candidate) =>
|
|
213
|
+
candidate.repoId === undefined ? [] : [candidate.repoId]))].sort((a, b) => a - b);
|
|
214
|
+
if (repoIds.length === 0) return [];
|
|
215
|
+
const placeholders = repoIds.map(() => '?').join(',');
|
|
216
|
+
const rows = db.prepare(`SELECT id repoId,name repoName,package_name packageName
|
|
217
|
+
FROM repositories WHERE workspace_id IN (
|
|
218
|
+
SELECT DISTINCT workspace_id FROM repositories WHERE id IN (${placeholders})
|
|
219
|
+
) ORDER BY workspace_id,name,absolute_path,id`).all(...repoIds);
|
|
220
|
+
return rows.flatMap((row): RepositoryIdentity[] => {
|
|
221
|
+
const repoId = numberValue(row.repoId);
|
|
222
|
+
const repoName = stringValue(row.repoName);
|
|
223
|
+
return repoId === undefined || !repoName ? [] : [{
|
|
224
|
+
repoId,
|
|
225
|
+
repoName,
|
|
226
|
+
packageName: stringValue(row.packageName),
|
|
227
|
+
}];
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function deduplicateProposals(rows: IdentityProposal[]): IdentityProposal[] {
|
|
232
|
+
const sorted = [...rows].sort((left, right) =>
|
|
233
|
+
left.operationId - right.operationId
|
|
234
|
+
|| left.key.localeCompare(right.key)
|
|
235
|
+
|| left.value.localeCompare(right.value)
|
|
236
|
+
|| left.provenance.sourceKind.localeCompare(right.provenance.sourceKind));
|
|
237
|
+
const seen = new Set<string>();
|
|
238
|
+
return sorted.filter((row) => {
|
|
239
|
+
const key = [row.operationId, row.key, row.value, row.normalizedIdentity].join(':');
|
|
240
|
+
if (seen.has(key)) return false;
|
|
241
|
+
seen.add(key);
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function routeOwnerAgrees(
|
|
247
|
+
candidate: DynamicTargetCandidate,
|
|
248
|
+
implementation: RouteImplementationEvidence | undefined,
|
|
249
|
+
): boolean {
|
|
250
|
+
return candidate.repoId !== undefined
|
|
251
|
+
&& implementation?.routeRepoId === candidate.repoId
|
|
252
|
+
&& implementation.edgeStatus === 'resolved'
|
|
253
|
+
&& candidate.viable
|
|
254
|
+
&& candidate.reasons.includes('service_path_template_match');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function routeImplementationEvidence(
|
|
258
|
+
db: Db,
|
|
259
|
+
operationId: number,
|
|
260
|
+
): RouteImplementationEvidence | undefined {
|
|
261
|
+
const rows = db.prepare(
|
|
262
|
+
`SELECT s.repo_id routeRepoId,o.provenance operationProvenance,
|
|
263
|
+
o.base_operation_id baseOperationId,e.status edgeStatus,
|
|
264
|
+
r.name handlerRepo
|
|
265
|
+
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id
|
|
266
|
+
JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER'
|
|
267
|
+
AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
|
|
268
|
+
JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
|
|
269
|
+
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
270
|
+
JOIN repositories r ON r.id=hc.repo_id
|
|
271
|
+
WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved'
|
|
272
|
+
AND o.id=?
|
|
273
|
+
ORDER BY e.id,hm.id`,
|
|
274
|
+
).all(String(operationId));
|
|
275
|
+
if (rows.length !== 1) return undefined;
|
|
276
|
+
const row = rows[0];
|
|
277
|
+
if (!row) return undefined;
|
|
278
|
+
return {
|
|
279
|
+
routeRepoId: numberValue(row.routeRepoId),
|
|
280
|
+
handlerRepo: stringValue(row.handlerRepo),
|
|
281
|
+
operationProvenance: stringValue(row.operationProvenance),
|
|
282
|
+
baseOperationId: numberValue(row.baseOperationId),
|
|
283
|
+
edgeStatus: stringValue(row.edgeStatus),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeIdentity(value: string, npmPackage = false): string {
|
|
288
|
+
const unscoped = npmPackage && /^@[^/]+\/[^/]+$/.test(value)
|
|
289
|
+
? value.slice(value.indexOf('/') + 1)
|
|
290
|
+
: value;
|
|
291
|
+
return unscoped
|
|
292
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
293
|
+
.toLowerCase()
|
|
294
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
295
|
+
.replace(/^_+|_+$/g, '');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function stringValue(value: unknown): string | undefined {
|
|
299
|
+
return typeof value === 'string' ? value : undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function numberValue(value: unknown): number | undefined {
|
|
303
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function escapeRegex(value: string): string {
|
|
307
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
|
|
3
|
+
export function loadTraceDiagnostics(
|
|
4
|
+
db: Db,
|
|
5
|
+
repoId: number | undefined,
|
|
6
|
+
includeWorkspaceDiagnostics: boolean,
|
|
7
|
+
workspaceId?: number,
|
|
8
|
+
): Array<Record<string, unknown>> {
|
|
9
|
+
if (repoId === undefined && !includeWorkspaceDiagnostics) return [];
|
|
10
|
+
return db.prepare(`SELECT d.repo_id repoId,d.severity,d.code,d.message,
|
|
11
|
+
d.source_file sourceFile,d.source_line sourceLine
|
|
12
|
+
FROM diagnostics d LEFT JOIN repositories r ON r.id=d.repo_id
|
|
13
|
+
WHERE (? IS NULL OR d.repo_id=?)
|
|
14
|
+
AND (? IS NULL OR d.repo_id IS NULL OR r.workspace_id=?)
|
|
15
|
+
ORDER BY severity,code,COALESCE(source_file,''),
|
|
16
|
+
COALESCE(source_line,0),d.id`).all(
|
|
17
|
+
repoId, repoId, workspaceId, workspaceId,
|
|
18
|
+
) as Array<
|
|
19
|
+
Record<string, unknown>
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function prependTraceDiagnostic(
|
|
24
|
+
diagnostics: Array<Record<string, unknown>>,
|
|
25
|
+
diagnostic: Record<string, unknown>,
|
|
26
|
+
): void {
|
|
27
|
+
const duplicate = diagnostics.findIndex((item) =>
|
|
28
|
+
sameDiagnosticLocation(item, diagnostic));
|
|
29
|
+
if (duplicate >= 0) diagnostics.splice(duplicate, 1);
|
|
30
|
+
diagnostics.unshift(diagnostic);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameDiagnosticLocation(
|
|
34
|
+
left: Record<string, unknown>,
|
|
35
|
+
right: Record<string, unknown>,
|
|
36
|
+
): boolean {
|
|
37
|
+
if (left.code !== right.code) return false;
|
|
38
|
+
const leftFile = stringValue(left.sourceFile);
|
|
39
|
+
const rightFile = stringValue(right.sourceFile);
|
|
40
|
+
if (leftFile || rightFile)
|
|
41
|
+
return leftFile === rightFile
|
|
42
|
+
&& numericValue(left.sourceLine) === numericValue(right.sourceLine);
|
|
43
|
+
return String(left.message ?? '') === String(right.message ?? '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stringValue(value: unknown): string | undefined {
|
|
47
|
+
return typeof value === 'string' ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function numericValue(value: unknown): number | undefined {
|
|
51
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
52
|
+
? value
|
|
53
|
+
: undefined;
|
|
54
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import {
|
|
3
|
+
projectBounded,
|
|
4
|
+
type BoundedProjection,
|
|
5
|
+
} from '../utils/000-bounded-projection.js';
|
|
6
|
+
import type { DynamicVariableProvenance } from './000-dynamic-target-types.js';
|
|
7
|
+
|
|
8
|
+
export interface DynamicReferenceRow {
|
|
9
|
+
bindingId?: number;
|
|
10
|
+
alias?: string;
|
|
11
|
+
aliasExpr?: string;
|
|
12
|
+
destination?: string;
|
|
13
|
+
servicePath?: string;
|
|
14
|
+
sourceKind: 'service_binding' | 'cds_require';
|
|
15
|
+
selection: 'selected_binding' | 'selected_binding_require' | 'fallback';
|
|
16
|
+
repoName: string;
|
|
17
|
+
sourceFile?: string;
|
|
18
|
+
sourceLine?: number;
|
|
19
|
+
helperChain?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DynamicRoutingContext {
|
|
23
|
+
outboundCallId?: number;
|
|
24
|
+
callerRepoId?: number;
|
|
25
|
+
callerRepo?: string;
|
|
26
|
+
selectedBindingId?: number;
|
|
27
|
+
bindingResolutionStatus: string;
|
|
28
|
+
selectedBinding?: DynamicReferenceRow;
|
|
29
|
+
bindingAlternatives: Array<Record<string, unknown>>;
|
|
30
|
+
bindingAlternativeCount: number;
|
|
31
|
+
shownBindingAlternativeCount: number;
|
|
32
|
+
omittedBindingAlternativeCount: number;
|
|
33
|
+
references: DynamicReferenceRow[];
|
|
34
|
+
fallbackUsed: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SelectedDynamicReference extends DynamicReferenceRow {
|
|
38
|
+
outboundCallId: number;
|
|
39
|
+
callerRepoId: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function dynamicRoutingContext(
|
|
43
|
+
db: Db,
|
|
44
|
+
workspaceId: number | undefined,
|
|
45
|
+
evidence: Record<string, unknown>,
|
|
46
|
+
): DynamicRoutingContext {
|
|
47
|
+
const selected = selectedBinding(db, workspaceId, evidence);
|
|
48
|
+
const persisted = persistedBindingResolution(evidence);
|
|
49
|
+
const alternatives = boundedAlternatives(
|
|
50
|
+
persisted.candidates,
|
|
51
|
+
persisted.candidateCount,
|
|
52
|
+
);
|
|
53
|
+
if (selected) {
|
|
54
|
+
const requires = exactRequireReferences(db, workspaceId, selected);
|
|
55
|
+
return {
|
|
56
|
+
...contextBase(selected, persisted.status, alternatives),
|
|
57
|
+
selectedBinding: selected,
|
|
58
|
+
references: [selected, ...requires],
|
|
59
|
+
fallbackUsed: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const callerRepoId = numberValue(evidence.repoId);
|
|
63
|
+
const callerRepo = stringValue(evidence.repo);
|
|
64
|
+
return {
|
|
65
|
+
outboundCallId: numberValue(evidence.outboundCallId ?? evidence.callId),
|
|
66
|
+
callerRepoId,
|
|
67
|
+
callerRepo,
|
|
68
|
+
bindingResolutionStatus: persisted.status,
|
|
69
|
+
bindingAlternatives: alternatives.items,
|
|
70
|
+
bindingAlternativeCount: alternatives.totalCount,
|
|
71
|
+
shownBindingAlternativeCount: alternatives.shownCount,
|
|
72
|
+
omittedBindingAlternativeCount: alternatives.omittedCount,
|
|
73
|
+
references: fallbackReferences(db, workspaceId, callerRepoId, callerRepo),
|
|
74
|
+
fallbackUsed: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function dynamicReferenceProvenance(
|
|
79
|
+
reference: DynamicReferenceRow,
|
|
80
|
+
kind: 'alias' | 'destination',
|
|
81
|
+
template: string,
|
|
82
|
+
value: string,
|
|
83
|
+
): DynamicVariableProvenance {
|
|
84
|
+
const sourceKind = reference.selection === 'selected_binding'
|
|
85
|
+
? `selected_binding.${kind}`
|
|
86
|
+
: reference.selection === 'selected_binding_require'
|
|
87
|
+
? `selected_binding_require.${kind}`
|
|
88
|
+
: `${reference.sourceKind}.${kind}`;
|
|
89
|
+
return {
|
|
90
|
+
sourceKind,
|
|
91
|
+
value,
|
|
92
|
+
rule: 'exact_indexed_reference_template_match',
|
|
93
|
+
template,
|
|
94
|
+
sourceRepo: reference.repoName,
|
|
95
|
+
sourceFile: reference.sourceFile,
|
|
96
|
+
sourceLine: reference.sourceLine,
|
|
97
|
+
selection: reference.selection,
|
|
98
|
+
bindingId: reference.bindingId,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function contextBase(
|
|
103
|
+
selected: SelectedDynamicReference,
|
|
104
|
+
status: string,
|
|
105
|
+
alternatives: ReturnType<typeof boundedAlternatives>,
|
|
106
|
+
): Omit<DynamicRoutingContext, 'selectedBinding' | 'references' | 'fallbackUsed'> {
|
|
107
|
+
return {
|
|
108
|
+
outboundCallId: selected.outboundCallId,
|
|
109
|
+
callerRepoId: selected.callerRepoId,
|
|
110
|
+
callerRepo: selected.repoName,
|
|
111
|
+
selectedBindingId: selected.bindingId,
|
|
112
|
+
bindingResolutionStatus: status,
|
|
113
|
+
bindingAlternatives: alternatives.items,
|
|
114
|
+
bindingAlternativeCount: alternatives.totalCount,
|
|
115
|
+
shownBindingAlternativeCount: alternatives.shownCount,
|
|
116
|
+
omittedBindingAlternativeCount: alternatives.omittedCount,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function selectedBinding(
|
|
121
|
+
db: Db,
|
|
122
|
+
workspaceId: number | undefined,
|
|
123
|
+
evidence: Record<string, unknown>,
|
|
124
|
+
): SelectedDynamicReference | undefined {
|
|
125
|
+
const callId = numberValue(evidence.outboundCallId ?? evidence.callId);
|
|
126
|
+
if (callId === undefined) return undefined;
|
|
127
|
+
const row = db.prepare(`SELECT c.id outboundCallId,c.repo_id callerRepoId,r.name repoName,
|
|
128
|
+
b.id bindingId,b.alias,b.alias_expr aliasExpr,b.destination_expr destination,
|
|
129
|
+
b.service_path_expr servicePath,b.source_file sourceFile,b.source_line sourceLine,
|
|
130
|
+
b.helper_chain_json helperChainJson
|
|
131
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
132
|
+
JOIN service_bindings b ON b.id=c.service_binding_id AND b.repo_id=c.repo_id
|
|
133
|
+
WHERE c.id=? AND (? IS NULL OR r.workspace_id=?)`).get(
|
|
134
|
+
callId, workspaceId, workspaceId,
|
|
135
|
+
);
|
|
136
|
+
return selectedReferenceFromRow(row);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function selectedReferenceFromRow(
|
|
140
|
+
row: Record<string, unknown> | undefined,
|
|
141
|
+
): SelectedDynamicReference | undefined {
|
|
142
|
+
const outboundCallId = numberValue(row?.outboundCallId);
|
|
143
|
+
const callerRepoId = numberValue(row?.callerRepoId);
|
|
144
|
+
const reference = referenceFromRow(
|
|
145
|
+
row, 'service_binding', 'selected_binding',
|
|
146
|
+
)[0];
|
|
147
|
+
return reference && outboundCallId !== undefined && callerRepoId !== undefined
|
|
148
|
+
? { ...reference, outboundCallId, callerRepoId }
|
|
149
|
+
: undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function exactRequireReferences(
|
|
153
|
+
db: Db,
|
|
154
|
+
workspaceId: number | undefined,
|
|
155
|
+
selected: SelectedDynamicReference,
|
|
156
|
+
): DynamicReferenceRow[] {
|
|
157
|
+
if (!(selected.aliasExpr ?? selected.alias)) return [];
|
|
158
|
+
const rows = db.prepare(`SELECT req.alias,req.destination,req.service_path servicePath,
|
|
159
|
+
r.name repoName,'package.json' sourceFile,1 sourceLine
|
|
160
|
+
FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
|
|
161
|
+
WHERE req.repo_id=? AND (? IS NULL OR r.workspace_id=?)
|
|
162
|
+
ORDER BY req.alias,req.id`).all(
|
|
163
|
+
selected.callerRepoId, workspaceId, workspaceId,
|
|
164
|
+
);
|
|
165
|
+
return rows.flatMap((row) => referenceFromRow(
|
|
166
|
+
row, 'cds_require', 'selected_binding_require', selected.bindingId,
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function fallbackReferences(
|
|
171
|
+
db: Db,
|
|
172
|
+
workspaceId: number | undefined,
|
|
173
|
+
callerRepoId: number | undefined,
|
|
174
|
+
callerRepo: string | undefined,
|
|
175
|
+
): DynamicReferenceRow[] {
|
|
176
|
+
if (callerRepoId === undefined && callerRepo === undefined) return [];
|
|
177
|
+
const rows = db.prepare(`SELECT b.id bindingId,COALESCE(b.alias,b.alias_expr) alias,
|
|
178
|
+
b.alias_expr aliasExpr,b.destination_expr destination,b.service_path_expr servicePath,
|
|
179
|
+
'service_binding' sourceKind,r.name repoName,b.source_file sourceFile,
|
|
180
|
+
b.source_line sourceLine,b.helper_chain_json helperChainJson,0 sourcePriority
|
|
181
|
+
FROM service_bindings b JOIN repositories r ON r.id=b.repo_id
|
|
182
|
+
WHERE (? IS NULL OR r.workspace_id=?)
|
|
183
|
+
AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
|
|
184
|
+
UNION ALL
|
|
185
|
+
SELECT NULL,req.alias,req.alias,req.destination,req.service_path,
|
|
186
|
+
'cds_require',r.name,'package.json',1,NULL,1
|
|
187
|
+
FROM cds_requires req JOIN repositories r ON r.id=req.repo_id
|
|
188
|
+
WHERE (? IS NULL OR r.workspace_id=?)
|
|
189
|
+
AND ((? IS NOT NULL AND r.id=?) OR (? IS NULL AND r.name=?))
|
|
190
|
+
ORDER BY sourcePriority,repoName,sourceFile,sourceLine`).all(
|
|
191
|
+
workspaceId, workspaceId, callerRepoId, callerRepoId, callerRepoId, callerRepo,
|
|
192
|
+
workspaceId, workspaceId, callerRepoId, callerRepoId, callerRepoId, callerRepo,
|
|
193
|
+
);
|
|
194
|
+
return rows.flatMap((row) => {
|
|
195
|
+
const sourceKind = row.sourceKind;
|
|
196
|
+
return sourceKind === 'service_binding' || sourceKind === 'cds_require'
|
|
197
|
+
? referenceFromRow(row, sourceKind, 'fallback')
|
|
198
|
+
: [];
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function referenceFromRow(
|
|
203
|
+
row: Record<string, unknown> | undefined,
|
|
204
|
+
sourceKind: DynamicReferenceRow['sourceKind'],
|
|
205
|
+
selection: DynamicReferenceRow['selection'],
|
|
206
|
+
bindingId = numberValue(row?.bindingId),
|
|
207
|
+
): DynamicReferenceRow[] {
|
|
208
|
+
const repoName = stringValue(row?.repoName);
|
|
209
|
+
if (!repoName) return [];
|
|
210
|
+
return [{
|
|
211
|
+
bindingId,
|
|
212
|
+
alias: stringValue(row?.alias),
|
|
213
|
+
aliasExpr: stringValue(row?.aliasExpr),
|
|
214
|
+
destination: stringValue(row?.destination),
|
|
215
|
+
servicePath: stringValue(row?.servicePath),
|
|
216
|
+
sourceKind,
|
|
217
|
+
selection,
|
|
218
|
+
repoName,
|
|
219
|
+
sourceFile: stringValue(row?.sourceFile),
|
|
220
|
+
sourceLine: numberValue(row?.sourceLine),
|
|
221
|
+
helperChain: parsedJson(row?.helperChainJson),
|
|
222
|
+
}];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function persistedBindingResolution(evidence: Record<string, unknown>): {
|
|
226
|
+
status: string;
|
|
227
|
+
candidates: Array<Record<string, unknown>>;
|
|
228
|
+
candidateCount: number;
|
|
229
|
+
} {
|
|
230
|
+
const outbound = record(evidence.outboundEvidence);
|
|
231
|
+
const resolution = record(outbound.serviceBindingResolution);
|
|
232
|
+
return {
|
|
233
|
+
status: stringValue(resolution.status) ?? 'unknown',
|
|
234
|
+
candidates: recordArray(resolution.candidates),
|
|
235
|
+
candidateCount: numberValue(resolution.candidateCount) ?? 0,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function boundedAlternatives(
|
|
240
|
+
rows: Array<Record<string, unknown>>,
|
|
241
|
+
reportedCount: number,
|
|
242
|
+
): BoundedProjection<Record<string, unknown>> {
|
|
243
|
+
const projection = projectBounded(rows, (left, right) =>
|
|
244
|
+
Number(left.bindingId ?? 0) - Number(right.bindingId ?? 0)
|
|
245
|
+
|| String(left.sourceFile ?? '').localeCompare(String(right.sourceFile ?? ''))
|
|
246
|
+
|| Number(left.sourceLine ?? 0) - Number(right.sourceLine ?? 0));
|
|
247
|
+
const totalCount = Math.max(reportedCount, projection.totalCount);
|
|
248
|
+
return {
|
|
249
|
+
...projection,
|
|
250
|
+
totalCount,
|
|
251
|
+
omittedCount: Math.max(0, totalCount - projection.shownCount),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parsedJson(value: unknown): unknown {
|
|
256
|
+
if (typeof value !== 'string' || value.length === 0) return undefined;
|
|
257
|
+
try {
|
|
258
|
+
return JSON.parse(value) as unknown;
|
|
259
|
+
} catch {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function record(value: unknown): Record<string, unknown> {
|
|
265
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
266
|
+
? value as Record<string, unknown>
|
|
267
|
+
: {};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function recordArray(value: unknown): Array<Record<string, unknown>> {
|
|
271
|
+
return Array.isArray(value)
|
|
272
|
+
? value.filter((item): item is Record<string, unknown> =>
|
|
273
|
+
Boolean(item && typeof item === 'object' && !Array.isArray(item)))
|
|
274
|
+
: [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function stringValue(value: unknown): string | undefined {
|
|
278
|
+
return typeof value === 'string' ? value : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function numberValue(value: unknown): number | undefined {
|
|
282
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
283
|
+
}
|