@saptools/service-flow 0.1.70 → 0.1.73
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 +21 -0
- package/README.md +23 -6
- package/TECHNICAL-NOTE.md +24 -2
- package/dist/{chunk-GSLFY6J2.js → chunk-32WOTGTS.js} +12061 -9288
- package/dist/chunk-32WOTGTS.js.map +1 -0
- package/dist/cli.js +854 -161
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -14
- package/dist/index.js +2 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/doctor-event-quality.ts +446 -0
- package/src/cli/doctor.ts +4 -6
- package/src/cli.ts +35 -13
- package/src/config/workspace-config.ts +15 -0
- package/src/db/call-fact-repository.ts +6 -3
- package/src/db/current-fact-semantics.ts +5 -29
- package/src/db/event-fact-semantics.ts +546 -0
- package/src/db/event-site-semantics.ts +62 -0
- package/src/db/event-surface-invalidation.ts +38 -0
- package/src/db/fact-json-inventory.ts +16 -0
- package/src/db/fact-lifecycle.ts +70 -12
- package/src/db/migrations.ts +15 -1
- package/src/db/package-target-invalidation.ts +80 -0
- package/src/db/repositories.ts +28 -2
- package/src/db/schema-structure.ts +41 -1
- package/src/db/schema.ts +6 -2
- package/src/indexer/repository-indexer.ts +93 -10
- package/src/indexer/workspace-indexer.ts +13 -3
- package/src/linker/cross-repo-linker.ts +27 -3
- package/src/linker/event-environment-link.ts +211 -0
- package/src/linker/event-shape-candidate-linker.ts +204 -0
- package/src/linker/event-subscription-handler-linker.ts +220 -25
- package/src/linker/event-template-link.ts +40 -6
- package/src/linker/package-event-constant-resolver.ts +302 -0
- package/src/output/repository-inspection.ts +11 -0
- package/src/output/table-output.ts +13 -1
- package/src/parsers/decorator-parser.ts +9 -53
- package/src/parsers/environment-declarations.ts +404 -0
- package/src/parsers/event-call-analysis.ts +370 -0
- package/src/parsers/event-environment-reference.ts +242 -0
- package/src/parsers/event-loop-registration.ts +132 -0
- package/src/parsers/event-name-import-resolution.ts +243 -0
- package/src/parsers/event-receiver-analysis.ts +394 -0
- package/src/parsers/event-subscription-facts.ts +4 -0
- package/src/parsers/generated-constants-parser.ts +80 -14
- package/src/parsers/outbound-call-classifier.ts +27 -124
- package/src/parsers/outbound-call-parser.ts +13 -1
- package/src/parsers/outbound-expression-analysis.ts +2 -2
- package/src/parsers/stable-local-value.ts +42 -10
- package/src/parsers/string-constant-lookups.ts +408 -0
- package/src/trace/edge-target.ts +65 -0
- package/src/trace/event-runtime-resolution.ts +24 -3
- package/src/trace/event-shape-candidate-trace.ts +172 -0
- package/src/trace/event-subscriber-traversal.ts +90 -8
- package/src/trace/evidence.ts +7 -28
- package/src/trace/trace-scope-execution.ts +22 -30
- package/src/types.ts +19 -1
- package/src/utils/event-skeleton.ts +207 -0
- package/src/version.ts +1 -1
- package/dist/chunk-GSLFY6J2.js.map +0 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
insertCalls,
|
|
8
8
|
insertExecutableSymbols,
|
|
9
9
|
insertHandler,
|
|
10
|
+
insertGeneratedConstants,
|
|
10
11
|
handlerMethodIsExecutable,
|
|
11
12
|
insertRegistrations,
|
|
12
13
|
insertSymbolCalls,
|
|
@@ -23,6 +24,9 @@ import {
|
|
|
23
24
|
parseOutboundCalls,
|
|
24
25
|
} from '../parsers/outbound-call-parser.js';
|
|
25
26
|
import { parseExecutableSymbols } from '../parsers/symbol-parser.js';
|
|
27
|
+
import {
|
|
28
|
+
generatedConstantFacts,
|
|
29
|
+
} from '../parsers/generated-constants-parser.js';
|
|
26
30
|
import {
|
|
27
31
|
loadPackageJsonSnapshot,
|
|
28
32
|
} from '../parsers/package-json-parser.js';
|
|
@@ -52,9 +56,22 @@ import {
|
|
|
52
56
|
import {
|
|
53
57
|
loadRepositorySourceContext,
|
|
54
58
|
type RepositorySourceContext,
|
|
59
|
+
type SourceFileSnapshot,
|
|
55
60
|
type SourceContextInstrumentation,
|
|
56
61
|
} from '../parsers/ts-project.js';
|
|
57
|
-
import
|
|
62
|
+
import {
|
|
63
|
+
createImportedEventNameResolver,
|
|
64
|
+
} from '../parsers/event-name-import-resolution.js';
|
|
65
|
+
import {
|
|
66
|
+
collectEnvironmentDeclarations,
|
|
67
|
+
type EnvironmentDeclarationsFact,
|
|
68
|
+
} from '../parsers/environment-declarations.js';
|
|
69
|
+
import {
|
|
70
|
+
createEventEnvironmentReferenceResolver,
|
|
71
|
+
} from '../parsers/event-environment-reference.js';
|
|
72
|
+
import { invalidateEventSurfaceFacts } from
|
|
73
|
+
'../db/event-surface-invalidation.js';
|
|
74
|
+
import type { CdsServiceFact, GeneratedConstantFact, HandlerClassFact, HandlerRegistrationFact, OutboundCallFact, PackageFacts, ServiceBindingFact, ExecutableSymbolFact, SymbolCallFact } from '../types.js';
|
|
58
75
|
export interface IndexRepoResult {
|
|
59
76
|
fileCount: number;
|
|
60
77
|
diagnosticCount: number;
|
|
@@ -68,6 +85,7 @@ interface ParsedFacts {
|
|
|
68
85
|
calls: OutboundCallFact[];
|
|
69
86
|
symbols: ExecutableSymbolFact[];
|
|
70
87
|
symbolCalls: SymbolCallFact[];
|
|
88
|
+
generatedConstants: GeneratedConstantFact[];
|
|
71
89
|
fileRecords: Array<{ relativePath: string; extension: string; sha256: string; sizeBytes: number }>;
|
|
72
90
|
}
|
|
73
91
|
export interface PreparedRepositoryIndex extends IndexRepoResult {
|
|
@@ -77,6 +95,7 @@ export interface PreparedRepositoryIndex extends IndexRepoResult {
|
|
|
77
95
|
kind?: string;
|
|
78
96
|
parsed?: ParsedFacts;
|
|
79
97
|
packagePublicSurface?: PackagePublicSurfaceFact;
|
|
98
|
+
environmentDeclarations?: EnvironmentDeclarationsFact;
|
|
80
99
|
}
|
|
81
100
|
export async function indexRepository(
|
|
82
101
|
db: Db,
|
|
@@ -110,6 +129,7 @@ export async function prepareRepositoryIndex(
|
|
|
110
129
|
repo: RepoRow,
|
|
111
130
|
force: boolean,
|
|
112
131
|
instrumentation?: SourceContextInstrumentation,
|
|
132
|
+
eventEnvironmentKeys?: readonly string[],
|
|
113
133
|
): Promise<PreparedRepositoryIndex> {
|
|
114
134
|
const sourceFiles = await findSourceFiles(repo.absolute_path);
|
|
115
135
|
const packageSnapshot = await loadPackageJsonSnapshot(repo.absolute_path, {
|
|
@@ -120,11 +140,17 @@ export async function prepareRepositoryIndex(
|
|
|
120
140
|
const sources = await loadRepositorySourceContext(
|
|
121
141
|
repo.absolute_path, sourceFiles, instrumentation,
|
|
122
142
|
);
|
|
143
|
+
const environmentDeclarations = collectEnvironmentDeclarations(
|
|
144
|
+
sources, eventEnvironmentKeys,
|
|
145
|
+
);
|
|
123
146
|
const fingerprint = repositoryFingerprint(
|
|
124
|
-
sources, packageFacts, packageSnapshot.rawText,
|
|
147
|
+
sources, packageFacts, packageSnapshot.rawText, environmentDeclarations,
|
|
125
148
|
);
|
|
126
149
|
if (!force && repo.fingerprint === fingerprint) return { repo, fileCount: 0, diagnosticCount: 0, skipped: true };
|
|
127
|
-
const parsedFacts = await parseAllSourceFacts(
|
|
150
|
+
const parsedFacts = await parseAllSourceFacts(
|
|
151
|
+
repo.absolute_path, sources, environmentDeclarations,
|
|
152
|
+
eventEnvironmentKeys,
|
|
153
|
+
);
|
|
128
154
|
const packageSurface = analyzeRepositoryPackageSurface(
|
|
129
155
|
packageFacts, packageSnapshot.manifest, sources,
|
|
130
156
|
);
|
|
@@ -139,6 +165,7 @@ export async function prepareRepositoryIndex(
|
|
|
139
165
|
kind: await classifyRepository(repo.absolute_path, packageFacts),
|
|
140
166
|
parsed,
|
|
141
167
|
packagePublicSurface: packageSurface.surface,
|
|
168
|
+
environmentDeclarations,
|
|
142
169
|
fileCount: sourceFiles.length,
|
|
143
170
|
diagnosticCount: parsed.handlers.filter((handler) =>
|
|
144
171
|
handler.hasHandlerDecorator
|
|
@@ -154,20 +181,27 @@ export function publishPreparedRepositoryIndex(
|
|
|
154
181
|
): void {
|
|
155
182
|
if (prepared.skipped) return;
|
|
156
183
|
if (!prepared.packageFacts || !prepared.parsed || !prepared.fingerprint
|
|
157
|
-
|| !prepared.kind || !prepared.packagePublicSurface
|
|
184
|
+
|| !prepared.kind || !prepared.packagePublicSurface
|
|
185
|
+
|| !prepared.environmentDeclarations)
|
|
158
186
|
throw new Error('Prepared repository index is missing publication facts');
|
|
159
187
|
const now = new Date().toISOString();
|
|
160
188
|
const repoId = prepared.repo.id;
|
|
189
|
+
const environmentJson = JSON.stringify(prepared.environmentDeclarations);
|
|
161
190
|
invalidatePackageTargetFacts(
|
|
162
191
|
db, repoId, prepared.packageFacts.packageName, invalidations,
|
|
163
192
|
);
|
|
193
|
+
invalidateEventSurfaceFacts(
|
|
194
|
+
db, repoId, prepared.parsed.calls, environmentJson,
|
|
195
|
+
);
|
|
164
196
|
db.prepare(`UPDATE repositories SET package_name=?, package_version=?,
|
|
165
|
-
dependencies_json=?,package_public_surface_json=?,
|
|
197
|
+
dependencies_json=?,package_public_surface_json=?,
|
|
198
|
+
environment_declarations_json=?,kind=?,index_status=?
|
|
166
199
|
WHERE id=?`).run(
|
|
167
200
|
prepared.packageFacts.packageName,
|
|
168
201
|
prepared.packageFacts.packageVersion,
|
|
169
202
|
JSON.stringify(prepared.packageFacts.dependencies),
|
|
170
203
|
JSON.stringify(prepared.packagePublicSurface),
|
|
204
|
+
environmentJson,
|
|
171
205
|
prepared.kind,
|
|
172
206
|
'indexing',
|
|
173
207
|
repoId,
|
|
@@ -183,6 +217,7 @@ export function publishPreparedRepositoryIndex(
|
|
|
183
217
|
insertRegistrations(db, repoId, prepared.parsed.registrations);
|
|
184
218
|
insertBindings(db, repoId, prepared.parsed.bindings);
|
|
185
219
|
insertCalls(db, repoId, prepared.parsed.calls);
|
|
220
|
+
insertGeneratedConstants(db, repoId, prepared.parsed.generatedConstants);
|
|
186
221
|
db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run(now, prepared.fingerprint, now, ANALYZER_VERSION, repoId);
|
|
187
222
|
}
|
|
188
223
|
|
|
@@ -242,15 +277,31 @@ export function recordIndexFailure(db: Db, repoId: number, error: unknown): void
|
|
|
242
277
|
async function parseAllSourceFacts(
|
|
243
278
|
root: string,
|
|
244
279
|
sources: RepositorySourceContext,
|
|
280
|
+
environmentDeclarations: EnvironmentDeclarationsFact,
|
|
281
|
+
eventEnvironmentKeys?: readonly string[],
|
|
245
282
|
): Promise<ParsedFacts> {
|
|
246
|
-
const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
|
|
283
|
+
const facts: ParsedFacts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], generatedConstants: [], fileRecords: [] };
|
|
247
284
|
for (const snapshot of sources.entries()) {
|
|
248
285
|
const file = snapshot.filePath;
|
|
249
|
-
facts.fileRecords.push({
|
|
286
|
+
facts.fileRecords.push({
|
|
287
|
+
relativePath: normalizePath(file),
|
|
288
|
+
extension: path.extname(file),
|
|
289
|
+
sha256: sourceFactHash(snapshot, environmentDeclarations),
|
|
290
|
+
sizeBytes: snapshot.sizeBytes,
|
|
291
|
+
});
|
|
250
292
|
if (file.endsWith('.cds')) facts.services.push(...(await parseCdsFile(root, file, sources)));
|
|
251
293
|
if (/\.[jt]s$/.test(file)) {
|
|
252
294
|
const source = snapshot.sourceFile();
|
|
253
|
-
|
|
295
|
+
facts.generatedConstants.push(...generatedConstantFacts(source, file));
|
|
296
|
+
const classified = classifyOutboundCallsInSource(source, file, {
|
|
297
|
+
importedEventNameResolver: createImportedEventNameResolver(
|
|
298
|
+
sources, source, file,
|
|
299
|
+
),
|
|
300
|
+
eventEnvironmentReferenceResolver:
|
|
301
|
+
createEventEnvironmentReferenceResolver(
|
|
302
|
+
sources, source, file, eventEnvironmentKeys,
|
|
303
|
+
),
|
|
304
|
+
});
|
|
254
305
|
facts.handlers.push(...(await parseDecorators(root, file, sources)));
|
|
255
306
|
facts.registrations.push(...(await parseHandlerRegistrations(root, file, sources)));
|
|
256
307
|
const bindings = await parseServiceBindings(root, file, sources);
|
|
@@ -280,12 +331,17 @@ async function findSourceFiles(root: string): Promise<string[]> {
|
|
|
280
331
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
281
332
|
if (e.isDirectory()) {
|
|
282
333
|
if (!['node_modules', 'dist', 'gen', 'coverage', '.git'].includes(e.name)) await walk(path.join(dir, e.name), rel);
|
|
283
|
-
} else if (
|
|
334
|
+
} else if (isRepositoryFactInput(e.name)
|
|
335
|
+
&& !isDefaultTestFile(rel)) out.push(rel);
|
|
284
336
|
}
|
|
285
337
|
}
|
|
286
338
|
await walk(root);
|
|
287
339
|
return out.sort();
|
|
288
340
|
}
|
|
341
|
+
function isRepositoryFactInput(name: string): boolean {
|
|
342
|
+
return /\.(cds|ts|js)$/.test(name)
|
|
343
|
+
|| ['nodemon.json', '.env', 'mta.yaml', 'manifest.yml'].includes(name);
|
|
344
|
+
}
|
|
289
345
|
function isDefaultTestFile(relativeFile: string): boolean {
|
|
290
346
|
const parts = relativeFile.split('/');
|
|
291
347
|
if (parts.some((part) => ['test', 'tests', '__tests__'].includes(part))) return true;
|
|
@@ -295,6 +351,7 @@ function repositoryFingerprint(
|
|
|
295
351
|
sources: RepositorySourceContext,
|
|
296
352
|
facts: PackageFacts,
|
|
297
353
|
packageJsonText: string,
|
|
354
|
+
environmentDeclarations: EnvironmentDeclarationsFact,
|
|
298
355
|
): string {
|
|
299
356
|
const normalizedFacts = {
|
|
300
357
|
analyzerVersion: ANALYZER_VERSION,
|
|
@@ -304,10 +361,36 @@ function repositoryFingerprint(
|
|
|
304
361
|
cdsRequires: [...facts.cdsRequires].sort((a, b) => a.alias.localeCompare(b.alias)),
|
|
305
362
|
scripts: Object.fromEntries(Object.entries(facts.scripts).sort()),
|
|
306
363
|
includeTests: false,
|
|
364
|
+
eventEnvironmentKeys: environmentDeclarations.allowedKeys,
|
|
307
365
|
packageJsonHash: sha256Text(packageJsonText),
|
|
308
366
|
};
|
|
309
367
|
const entries: string[] = [`facts:${JSON.stringify(normalizedFacts)}`];
|
|
310
368
|
for (const snapshot of sources.entries())
|
|
311
|
-
entries.push(
|
|
369
|
+
entries.push(
|
|
370
|
+
`${snapshot.filePath}:${
|
|
371
|
+
sourceFactHash(snapshot, environmentDeclarations)}`,
|
|
372
|
+
);
|
|
312
373
|
return sha256Text(entries.join('\n'));
|
|
313
374
|
}
|
|
375
|
+
|
|
376
|
+
function sourceFactHash(
|
|
377
|
+
snapshot: SourceFileSnapshot,
|
|
378
|
+
environmentDeclarations: EnvironmentDeclarationsFact,
|
|
379
|
+
): string {
|
|
380
|
+
if (!isEnvironmentFactInput(snapshot.filePath))
|
|
381
|
+
return sha256Text(snapshot.text);
|
|
382
|
+
const declarations = environmentDeclarations.declarations.filter(
|
|
383
|
+
(item) => item.sourceFile === snapshot.filePath,
|
|
384
|
+
);
|
|
385
|
+
return sha256Text(JSON.stringify({
|
|
386
|
+
allowedKeys: environmentDeclarations.allowedKeys,
|
|
387
|
+
declarations,
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isEnvironmentFactInput(filePath: string): boolean {
|
|
392
|
+
const name = filePath.split('/').at(-1);
|
|
393
|
+
return ['nodemon.json', '.env', 'mta.yaml', 'manifest.yml'].includes(
|
|
394
|
+
name ?? '',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
@@ -104,7 +104,12 @@ export interface IndexWorkspaceSummary {
|
|
|
104
104
|
export async function indexWorkspace(
|
|
105
105
|
db: Db,
|
|
106
106
|
workspaceId: number,
|
|
107
|
-
options: {
|
|
107
|
+
options: {
|
|
108
|
+
repo?: string;
|
|
109
|
+
force: boolean;
|
|
110
|
+
eventEnvironmentKeys?: readonly string[];
|
|
111
|
+
injectDerivedMaterializationFailure?: boolean;
|
|
112
|
+
},
|
|
108
113
|
): Promise<IndexWorkspaceSummary> {
|
|
109
114
|
const repos = selectedRepositories(db, workspaceId, options.repo);
|
|
110
115
|
const runId = claimIndexRun(db, workspaceId, repos.length);
|
|
@@ -112,7 +117,9 @@ export async function indexWorkspace(
|
|
|
112
117
|
fileCount: 0, diagnosticCount: 0, skippedCount: 0, rows: [],
|
|
113
118
|
};
|
|
114
119
|
try {
|
|
115
|
-
await prepareRepositories(
|
|
120
|
+
await prepareRepositories(
|
|
121
|
+
repos, options.force, state, options.eventEnvironmentKeys,
|
|
122
|
+
);
|
|
116
123
|
return publishPreparedWorkspaceRows(
|
|
117
124
|
db, workspaceId, runId, state.rows, options,
|
|
118
125
|
);
|
|
@@ -168,10 +175,13 @@ async function prepareRepositories(
|
|
|
168
175
|
repos: readonly IndexRepository[],
|
|
169
176
|
force: boolean,
|
|
170
177
|
state: PreparationState,
|
|
178
|
+
eventEnvironmentKeys?: readonly string[],
|
|
171
179
|
): Promise<void> {
|
|
172
180
|
for (const repo of repos) {
|
|
173
181
|
state.activeRepoId = repo.id;
|
|
174
|
-
const result = await prepareRepositoryIndex(
|
|
182
|
+
const result = await prepareRepositoryIndex(
|
|
183
|
+
repo, force, undefined, eventEnvironmentKeys,
|
|
184
|
+
);
|
|
175
185
|
state.rows.push(result);
|
|
176
186
|
state.fileCount += result.fileCount;
|
|
177
187
|
state.diagnosticCount += result.diagnosticCount;
|
|
@@ -4,7 +4,10 @@ import { linkImplementations as linkCanonicalImplementations } from './implement
|
|
|
4
4
|
import { linkPackageImportSymbolCalls } from './package-import-symbol-resolver.js';
|
|
5
5
|
import { linkEventSubscriptionHandlers } from './event-subscription-handler-linker.js';
|
|
6
6
|
import { insertCallEdge } from './call-edge-insertion.js';
|
|
7
|
+
import { linkEventShapeCandidates } from './event-shape-candidate-linker.js';
|
|
7
8
|
import { assertWorkspaceLinkable } from '../db/fact-lifecycle.js';
|
|
9
|
+
import { linkPackageEventConstants } from
|
|
10
|
+
'./package-event-constant-resolver.js';
|
|
8
11
|
export interface LinkWorkspaceResult {
|
|
9
12
|
edgeCount: number;
|
|
10
13
|
unresolvedCount: number;
|
|
@@ -23,11 +26,14 @@ export interface LinkWorkspaceResult {
|
|
|
23
26
|
subscriptionHandlerAmbiguousCount: number;
|
|
24
27
|
subscriptionHandlerUnresolvedCount: number;
|
|
25
28
|
subscriptionHandlerMissingAssociationCount: number;
|
|
29
|
+
eventShapeCandidateCount: number;
|
|
30
|
+
eventShapeCandidateOmittedCount: number;
|
|
26
31
|
}
|
|
27
32
|
export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string, string> = {}): LinkWorkspaceResult {
|
|
28
33
|
return db.transaction(() => {
|
|
29
34
|
assertWorkspaceLinkable(db, workspaceId);
|
|
30
35
|
linkPackageImportSymbolCalls(db, workspaceId);
|
|
36
|
+
linkPackageEventConstants(db, workspaceId);
|
|
31
37
|
assertWorkspaceLinkable(db, workspaceId, 'terminal');
|
|
32
38
|
const generation = nextGraphGeneration(db, workspaceId);
|
|
33
39
|
db.prepare('DELETE FROM graph_edges WHERE workspace_id=?').run(workspaceId);
|
|
@@ -35,10 +41,13 @@ export function linkWorkspace(db: Db, workspaceId: number, vars: Record<string,
|
|
|
35
41
|
const subscriptions = linkEventSubscriptionHandlers(
|
|
36
42
|
db, workspaceId, generation, vars,
|
|
37
43
|
);
|
|
44
|
+
const eventShapes = linkEventShapeCandidates(
|
|
45
|
+
db, workspaceId, generation,
|
|
46
|
+
);
|
|
38
47
|
const impl = linkCanonicalImplementations(db, workspaceId, generation);
|
|
39
48
|
const callSummary = linkCalls(db, workspaceId, vars, generation);
|
|
40
49
|
db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
|
|
41
|
-
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount };
|
|
50
|
+
return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount + subscriptions.edgeCount + eventShapes.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount, subscriptionHandlerResolvedCount: subscriptions.resolvedCount, subscriptionHandlerAmbiguousCount: subscriptions.ambiguousCount, subscriptionHandlerUnresolvedCount: subscriptions.unresolvedCount, subscriptionHandlerMissingAssociationCount: subscriptions.missingAssociationCount, eventShapeCandidateCount: eventShapes.edgeCount, eventShapeCandidateOmittedCount: eventShapes.omittedCount };
|
|
42
51
|
});
|
|
43
52
|
}
|
|
44
53
|
function nextGraphGeneration(db: Db, workspaceId: number): number {
|
|
@@ -54,7 +63,9 @@ type CallLinkSummary = Omit<LinkWorkspaceResult,
|
|
|
54
63
|
| 'subscriptionHandlerResolvedCount'
|
|
55
64
|
| 'subscriptionHandlerAmbiguousCount'
|
|
56
65
|
| 'subscriptionHandlerUnresolvedCount'
|
|
57
|
-
| 'subscriptionHandlerMissingAssociationCount'
|
|
66
|
+
| 'subscriptionHandlerMissingAssociationCount'
|
|
67
|
+
| 'eventShapeCandidateCount'
|
|
68
|
+
| 'eventShapeCandidateOmittedCount'>;
|
|
58
69
|
|
|
59
70
|
function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, generation: number): CallLinkSummary {
|
|
60
71
|
let edgeCount = 0;
|
|
@@ -65,7 +76,20 @@ function linkCalls(db: Db, workspaceId: number, vars: Record<string, string>, ge
|
|
|
65
76
|
let ambiguousCount = 0;
|
|
66
77
|
let dynamicCount = 0;
|
|
67
78
|
let terminalCount = 0;
|
|
68
|
-
const calls = db.prepare(`SELECT c.*,r.name repoName,
|
|
79
|
+
const calls = db.prepare(`SELECT c.*,r.name repoName,
|
|
80
|
+
r.environment_declarations_json environmentDeclarationsJson,
|
|
81
|
+
b.id selectedBindingId,b.alias,b.alias_expr aliasExpr,
|
|
82
|
+
b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,
|
|
83
|
+
b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,
|
|
84
|
+
b.source_file bindingSourceFile,b.source_line bindingSourceLine,
|
|
85
|
+
b.helper_chain_json helperChainJson,req.service_path requireServicePath,
|
|
86
|
+
req.destination requireDestination
|
|
87
|
+
FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id
|
|
88
|
+
LEFT JOIN service_bindings b ON b.id=c.service_binding_id
|
|
89
|
+
LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias
|
|
90
|
+
WHERE r.workspace_id=?`).all(
|
|
91
|
+
workspaceId,
|
|
92
|
+
) as Array<Record<string, unknown>>;
|
|
69
93
|
for (const call of calls) {
|
|
70
94
|
const result = insertCallEdge(db, workspaceId, call, vars, generation);
|
|
71
95
|
edgeCount += 1;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { Db } from '../db/connection.js';
|
|
2
|
+
import {
|
|
3
|
+
applyEventEnvironmentTransforms,
|
|
4
|
+
type EventEnvironmentReference,
|
|
5
|
+
} from '../parsers/event-environment-reference.js';
|
|
6
|
+
import {
|
|
7
|
+
parseEnvironmentDeclarationsFact,
|
|
8
|
+
type EnvironmentDeclarationsFact,
|
|
9
|
+
} from '../parsers/environment-declarations.js';
|
|
10
|
+
import {
|
|
11
|
+
eventTemplateVariables,
|
|
12
|
+
parseEventSkeletonFact,
|
|
13
|
+
} from '../utils/event-skeleton.js';
|
|
14
|
+
|
|
15
|
+
export interface EventEnvironmentResolution {
|
|
16
|
+
status: 'resolved' | 'ambiguous' | 'unresolved' | 'not_applicable';
|
|
17
|
+
variables: Record<string, string>;
|
|
18
|
+
reason?: string;
|
|
19
|
+
provenance: Array<Record<string, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SubscriptionEnvironmentTarget {
|
|
23
|
+
consumerRepoId?: number;
|
|
24
|
+
consumerRepoName?: string;
|
|
25
|
+
resolution: EventEnvironmentResolution;
|
|
26
|
+
collisionCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
30
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
31
|
+
? value as Record<string, unknown> : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parsedJson(value: unknown): unknown {
|
|
35
|
+
if (typeof value !== 'string') return value;
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(value) as unknown;
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function bindingEnvironmentValues(
|
|
44
|
+
binding: EventEnvironmentReference,
|
|
45
|
+
fact: EnvironmentDeclarationsFact,
|
|
46
|
+
): string[] {
|
|
47
|
+
if (!binding.environmentKey) return [];
|
|
48
|
+
return [...new Set(fact.declarations
|
|
49
|
+
.filter((item) => item.key === binding.environmentKey)
|
|
50
|
+
.map((item) => item.value))];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function bindingResolution(
|
|
54
|
+
binding: EventEnvironmentReference,
|
|
55
|
+
fact: EnvironmentDeclarationsFact,
|
|
56
|
+
): { value?: string; reason?: string; ambiguous?: boolean } {
|
|
57
|
+
if (binding.status === 'refused')
|
|
58
|
+
return { reason: binding.reason ?? 'event_environment_reference_refused' };
|
|
59
|
+
if (fact.status === 'incomplete') return {
|
|
60
|
+
reason: 'event_environment_declarations_incomplete',
|
|
61
|
+
};
|
|
62
|
+
const values = bindingEnvironmentValues(binding, fact);
|
|
63
|
+
if (fact.status === 'ambiguous' || values.length > 1) return {
|
|
64
|
+
reason: 'event_environment_declaration_ambiguous', ambiguous: true,
|
|
65
|
+
};
|
|
66
|
+
const value = values[0];
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return { reason: 'event_environment_declaration_missing' };
|
|
69
|
+
return {
|
|
70
|
+
value: applyEventEnvironmentTransforms(value, binding.transforms),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function provenance(
|
|
75
|
+
binding: EventEnvironmentReference,
|
|
76
|
+
fact: EnvironmentDeclarationsFact,
|
|
77
|
+
): Record<string, unknown> {
|
|
78
|
+
const rank = new Map([
|
|
79
|
+
['env_declaration_mta', 0],
|
|
80
|
+
['env_declaration_manifest', 1],
|
|
81
|
+
['env_declaration_dotenv', 2],
|
|
82
|
+
['env_declaration_dev', 3],
|
|
83
|
+
]);
|
|
84
|
+
const declaration = fact.declarations.filter((item) =>
|
|
85
|
+
item.key === binding.environmentKey).sort((left, right) =>
|
|
86
|
+
(rank.get(left.provenance) ?? 99) - (rank.get(right.provenance) ?? 99)
|
|
87
|
+
|| (left.sourceFile < right.sourceFile
|
|
88
|
+
? -1 : left.sourceFile > right.sourceFile ? 1 : 0)
|
|
89
|
+
|| left.startOffset - right.startOffset)[0];
|
|
90
|
+
return {
|
|
91
|
+
sourceKey: binding.sourceKey,
|
|
92
|
+
environmentKey: binding.environmentKey,
|
|
93
|
+
transforms: binding.transforms,
|
|
94
|
+
declarationProvenance: declaration?.provenance,
|
|
95
|
+
declarationSourceFile: declaration?.sourceFile,
|
|
96
|
+
declarationStartOffset: declaration?.startOffset,
|
|
97
|
+
declarationEndOffset: declaration?.endOffset,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveEventEnvironment(
|
|
102
|
+
skeletonValue: unknown,
|
|
103
|
+
environmentValue: unknown,
|
|
104
|
+
variables: Record<string, string>,
|
|
105
|
+
): EventEnvironmentResolution {
|
|
106
|
+
const skeleton = parseEventSkeletonFact(skeletonValue);
|
|
107
|
+
if (!skeleton || skeleton.environmentBindings.length === 0) return {
|
|
108
|
+
status: 'not_applicable',
|
|
109
|
+
variables: eventTemplateVariables(skeleton, variables),
|
|
110
|
+
provenance: [],
|
|
111
|
+
};
|
|
112
|
+
const fact = parseEnvironmentDeclarationsFact(environmentValue);
|
|
113
|
+
if (!fact) return {
|
|
114
|
+
status: 'unresolved', variables,
|
|
115
|
+
reason: 'event_environment_declarations_invalid', provenance: [],
|
|
116
|
+
};
|
|
117
|
+
const expanded = eventTemplateVariables(skeleton, variables);
|
|
118
|
+
const outcomes = skeleton.environmentBindings.map((binding) =>
|
|
119
|
+
Object.hasOwn(expanded, binding.sourceKey)
|
|
120
|
+
? {} : bindingResolution(binding, fact));
|
|
121
|
+
for (let index = 0; index < outcomes.length; index += 1) {
|
|
122
|
+
const binding = skeleton.environmentBindings[index];
|
|
123
|
+
const outcome = outcomes[index];
|
|
124
|
+
if (binding?.sourceKey && outcome?.value !== undefined
|
|
125
|
+
&& !Object.hasOwn(expanded, binding.sourceKey))
|
|
126
|
+
expanded[binding.sourceKey] = outcome.value;
|
|
127
|
+
}
|
|
128
|
+
const ambiguous = outcomes.find((outcome) => outcome.ambiguous);
|
|
129
|
+
const unresolved = outcomes.find((outcome) => outcome.reason);
|
|
130
|
+
return {
|
|
131
|
+
status: ambiguous ? 'ambiguous'
|
|
132
|
+
: unresolved ? 'unresolved' : 'resolved',
|
|
133
|
+
variables: expanded,
|
|
134
|
+
reason: ambiguous?.reason ?? unresolved?.reason,
|
|
135
|
+
provenance: skeleton.environmentBindings.map((binding) =>
|
|
136
|
+
provenance(binding, fact)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function dependencies(value: unknown): Record<string, string> {
|
|
141
|
+
const parsed = record(parsedJson(value));
|
|
142
|
+
if (!parsed) return {};
|
|
143
|
+
return Object.fromEntries(Object.entries(parsed).flatMap(([key, item]) =>
|
|
144
|
+
typeof item === 'string' ? [[key, item]] : []));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function consumerRows(
|
|
148
|
+
db: Db,
|
|
149
|
+
workspaceId: number,
|
|
150
|
+
packageName: string,
|
|
151
|
+
): Array<Record<string, unknown>> {
|
|
152
|
+
return db.prepare(`SELECT id,name,dependencies_json dependenciesJson,
|
|
153
|
+
environment_declarations_json environmentJson
|
|
154
|
+
FROM repositories WHERE workspace_id=?
|
|
155
|
+
ORDER BY name COLLATE BINARY,id`).all(workspaceId)
|
|
156
|
+
.filter((row) => Object.hasOwn(
|
|
157
|
+
dependencies(row.dependenciesJson), packageName,
|
|
158
|
+
));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function collisionCounts(
|
|
162
|
+
targets: readonly SubscriptionEnvironmentTarget[],
|
|
163
|
+
): Map<string, number> {
|
|
164
|
+
const counts = new Map<string, number>();
|
|
165
|
+
for (const target of targets) {
|
|
166
|
+
const values = Object.entries(target.resolution.variables)
|
|
167
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
168
|
+
const key = JSON.stringify(values);
|
|
169
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
return counts;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function subscriptionEnvironmentTargets(
|
|
175
|
+
db: Db,
|
|
176
|
+
workspaceId: number,
|
|
177
|
+
packageName: string | undefined,
|
|
178
|
+
skeletonValue: unknown,
|
|
179
|
+
ownEnvironmentValue: unknown,
|
|
180
|
+
variables: Record<string, string>,
|
|
181
|
+
): SubscriptionEnvironmentTarget[] {
|
|
182
|
+
const skeleton = parseEventSkeletonFact(skeletonValue);
|
|
183
|
+
if (!skeleton || skeleton.environmentBindings.length === 0
|
|
184
|
+
|| !packageName) return [{
|
|
185
|
+
resolution: resolveEventEnvironment(
|
|
186
|
+
skeletonValue, ownEnvironmentValue, variables,
|
|
187
|
+
),
|
|
188
|
+
collisionCount: 1,
|
|
189
|
+
}];
|
|
190
|
+
const rows = consumerRows(db, workspaceId, packageName);
|
|
191
|
+
const provisional = rows.map((row): SubscriptionEnvironmentTarget => ({
|
|
192
|
+
consumerRepoId: Number(row.id),
|
|
193
|
+
consumerRepoName: String(row.name),
|
|
194
|
+
resolution: resolveEventEnvironment(
|
|
195
|
+
skeletonValue, row.environmentJson, variables,
|
|
196
|
+
),
|
|
197
|
+
collisionCount: 1,
|
|
198
|
+
}));
|
|
199
|
+
if (provisional.length === 0) return [{
|
|
200
|
+
resolution: resolveEventEnvironment(
|
|
201
|
+
skeletonValue, ownEnvironmentValue, variables,
|
|
202
|
+
),
|
|
203
|
+
collisionCount: 1,
|
|
204
|
+
}];
|
|
205
|
+
const counts = collisionCounts(provisional);
|
|
206
|
+
return provisional.map((target) => {
|
|
207
|
+
const key = JSON.stringify(Object.entries(target.resolution.variables)
|
|
208
|
+
.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
209
|
+
return { ...target, collisionCount: counts.get(key) ?? 1 };
|
|
210
|
+
});
|
|
211
|
+
}
|