@eventcatalog/core 4.7.3 → 4.7.5
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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-SMDTRRED.js → chunk-2BYTIH4L.js} +1 -1
- package/dist/chunk-A2RZR3U4.js +29 -0
- package/dist/{chunk-XXJX3WWF.js → chunk-EUZT4RKY.js} +1 -1
- package/dist/chunk-GYYYAUJD.js +276 -0
- package/dist/chunk-JJPB6EOZ.js +159 -0
- package/dist/{chunk-GINIJSFI.js → chunk-QJRY6Q7S.js} +1 -1
- package/dist/{chunk-IALEXWSE.js → chunk-QQPH6RCB.js} +24 -1
- package/dist/chunk-SDZQJTJW.js +90 -0
- package/dist/chunk-WRNH5C3U.js +118 -0
- package/dist/{chunk-V4FAVGI7.js → chunk-ZBPMCEXS.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +707 -255
- package/dist/eventcatalog.config.d.cts +12 -6
- package/dist/eventcatalog.config.d.ts +12 -6
- package/dist/eventcatalog.js +42 -42
- package/dist/federation/diagnostics.cjs +187 -0
- package/dist/federation/diagnostics.d.cts +23 -0
- package/dist/federation/diagnostics.d.ts +23 -0
- package/dist/federation/diagnostics.js +14 -0
- package/dist/federation/federate.cjs +605 -174
- package/dist/federation/federate.d.cts +8 -1
- package/dist/federation/federate.d.ts +8 -1
- package/dist/federation/federate.js +8 -2
- package/dist/federation/filesystem-source-provider.cjs +124 -0
- package/dist/federation/filesystem-source-provider.d.cts +7 -0
- package/dist/federation/filesystem-source-provider.d.ts +7 -0
- package/dist/federation/filesystem-source-provider.js +6 -0
- package/dist/federation/output-transaction.cjs +152 -0
- package/dist/federation/output-transaction.d.cts +3 -0
- package/dist/federation/output-transaction.d.ts +3 -0
- package/dist/federation/output-transaction.js +6 -0
- package/dist/federation/source-provider.cjs +265 -0
- package/dist/federation/source-provider.d.cts +11 -0
- package/dist/federation/source-provider.d.ts +11 -0
- package/dist/federation/source-provider.js +8 -0
- package/dist/generate.cjs +24 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +24 -1
- package/dist/utils/cli-logger.d.cts +6 -0
- package/dist/utils/cli-logger.d.ts +6 -0
- package/dist/utils/cli-logger.js +2 -2
- package/package.json +3 -3
- package/dist/chunk-ZR6AH5Z2.js +0 -204
|
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
140
140
|
var import_os = __toESM(require("os"), 1);
|
|
141
141
|
|
|
142
142
|
// package.json
|
|
143
|
-
var version = "4.7.
|
|
143
|
+
var version = "4.7.5";
|
|
144
144
|
|
|
145
145
|
// src/constants.ts
|
|
146
146
|
var VERSION = version;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-QJRY6Q7S.js";
|
|
4
|
+
import "../chunk-2BYTIH4L.js";
|
|
5
5
|
import "../chunk-K2XIENVT.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-ZBPMCEXS.js";
|
|
7
7
|
import "../chunk-6QENHZZP.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFileSystemSourceProvider
|
|
3
|
+
} from "./chunk-SDZQJTJW.js";
|
|
4
|
+
import {
|
|
5
|
+
createGitHubSourceProvider
|
|
6
|
+
} from "./chunk-352FGP6W.js";
|
|
7
|
+
|
|
8
|
+
// src/federation/source-provider.ts
|
|
9
|
+
var createFederationSourceProvider = (projectDirectory, providers = {}) => {
|
|
10
|
+
const github = providers.github ?? createGitHubSourceProvider();
|
|
11
|
+
const filesystem = providers.filesystem ?? createFileSystemSourceProvider(projectDirectory);
|
|
12
|
+
const getProvider = (source) => {
|
|
13
|
+
if (source.source.startsWith("github:")) return github;
|
|
14
|
+
if (source.source.startsWith("file:")) return filesystem;
|
|
15
|
+
throw new Error(`Unsupported federation source "${source.source}" for "${source.id}". Supported protocols: github:, file:.`);
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
async resolve(source) {
|
|
19
|
+
return getProvider(source).resolve(source);
|
|
20
|
+
},
|
|
21
|
+
async fetchContent(request) {
|
|
22
|
+
return getProvider(request.source).fetchContent(request);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
createFederationSourceProvider
|
|
29
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withFederationOutputTransaction
|
|
3
|
+
} from "./chunk-WRNH5C3U.js";
|
|
4
|
+
import {
|
|
5
|
+
composePublicAssets
|
|
6
|
+
} from "./chunk-5EPNFDT5.js";
|
|
7
|
+
import {
|
|
8
|
+
createFederationSourceProvider
|
|
9
|
+
} from "./chunk-A2RZR3U4.js";
|
|
10
|
+
import {
|
|
11
|
+
createFederationContentCache
|
|
12
|
+
} from "./chunk-R7Z5ALYI.js";
|
|
13
|
+
import {
|
|
14
|
+
getFederationDiagnostics,
|
|
15
|
+
resolveFederationRules
|
|
16
|
+
} from "./chunk-JJPB6EOZ.js";
|
|
17
|
+
import {
|
|
18
|
+
getEventCatalogConfigFile
|
|
19
|
+
} from "./chunk-6QENHZZP.js";
|
|
20
|
+
|
|
21
|
+
// src/federation/federate.ts
|
|
22
|
+
import { createHash } from "crypto";
|
|
23
|
+
import fs from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import createSDK, { hydrate, resolve } from "@eventcatalog/sdk";
|
|
26
|
+
var FederationConflictError = class extends Error {
|
|
27
|
+
conflicts;
|
|
28
|
+
constructor(conflicts) {
|
|
29
|
+
const details = conflicts.map((conflict) => `${conflict.id}: ${conflict.sources.join(", ")}`).join("\n");
|
|
30
|
+
super(`Federation conflicts prevent hydration:
|
|
31
|
+
${details}`);
|
|
32
|
+
this.name = "FederationConflictError";
|
|
33
|
+
this.conflicts = conflicts;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var FederationDiagnosticError = class extends Error {
|
|
37
|
+
diagnostics;
|
|
38
|
+
constructor(diagnostics) {
|
|
39
|
+
const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
|
|
40
|
+
super(`Federation diagnostics prevent hydration:
|
|
41
|
+
${details}`);
|
|
42
|
+
this.name = "FederationDiagnosticError";
|
|
43
|
+
this.diagnostics = diagnostics;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var validateSources = (sources) => {
|
|
47
|
+
const ids = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const source of sources) {
|
|
49
|
+
if (!source.id?.trim()) throw new Error("Every federation source requires a stable id.");
|
|
50
|
+
if (ids.has(source.id)) throw new Error(`Federation source id "${source.id}" is configured more than once.`);
|
|
51
|
+
ids.add(source.id);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
|
|
55
|
+
const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
|
|
56
|
+
const externalsByPointer = /* @__PURE__ */ new Map();
|
|
57
|
+
for (const edge of edges.filter((edge2) => edge2.status === "external")) {
|
|
58
|
+
const version = edge.pointer ?? void 0;
|
|
59
|
+
const key = `${edge.to}@${version ?? ""}`;
|
|
60
|
+
const external = externalsByPointer.get(key) ?? {
|
|
61
|
+
id: edge.to,
|
|
62
|
+
...version === void 0 ? {} : { version },
|
|
63
|
+
referencedBy: []
|
|
64
|
+
};
|
|
65
|
+
if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
|
|
66
|
+
externalsByPointer.set(key, external);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
...graph,
|
|
70
|
+
entities: [
|
|
71
|
+
...graph.entities,
|
|
72
|
+
...ownershipGraph.entities.filter(
|
|
73
|
+
(entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
|
|
74
|
+
)
|
|
75
|
+
],
|
|
76
|
+
edges,
|
|
77
|
+
conflicts: ownershipGraph.conflicts,
|
|
78
|
+
externals: [...externalsByPointer.values()]
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
var writeLock = async (lockPath, lock) => {
|
|
82
|
+
const temporaryPath = `${lockPath}.tmp-${process.pid}`;
|
|
83
|
+
try {
|
|
84
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
|
|
85
|
+
`, "utf8");
|
|
86
|
+
await fs.rename(temporaryPath, lockPath);
|
|
87
|
+
} finally {
|
|
88
|
+
await fs.rm(temporaryPath, { force: true });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var readLock = async (lockPath) => {
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error.code === "ENOENT") return void 0;
|
|
96
|
+
throw new Error(`Cannot read federation lock at "${lockPath}"`, { cause: error });
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var pathExists = async (filePath) => {
|
|
100
|
+
try {
|
|
101
|
+
await fs.access(filePath);
|
|
102
|
+
return true;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (error.code === "ENOENT") return false;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
|
|
109
|
+
const outDir = path.join(projectDirectory, "federated");
|
|
110
|
+
const lockPath = path.join(projectDirectory, "eventcatalog.lock");
|
|
111
|
+
const previousLock = await readLock(lockPath);
|
|
112
|
+
const hadFederatedOutput = await pathExists(outDir);
|
|
113
|
+
const hadLock = previousLock !== void 0;
|
|
114
|
+
if (!hadFederatedOutput && !hadLock) return;
|
|
115
|
+
const publicResult = await withFederationOutputTransaction(
|
|
116
|
+
projectDirectory,
|
|
117
|
+
outDir,
|
|
118
|
+
Object.keys(previousLock?.publicFiles ?? {}),
|
|
119
|
+
async () => {
|
|
120
|
+
const result = await composePublicAssets({
|
|
121
|
+
projectDirectory,
|
|
122
|
+
federatedDirectory: outDir,
|
|
123
|
+
assets: [],
|
|
124
|
+
previousFiles: previousLock?.publicFiles
|
|
125
|
+
});
|
|
126
|
+
await fs.rm(lockPath, { force: true });
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
onProgress?.({
|
|
131
|
+
type: "cleanup:complete",
|
|
132
|
+
federated: hadFederatedOutput,
|
|
133
|
+
publicFiles: publicResult.removed,
|
|
134
|
+
lock: hadLock
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
var federateCatalog = async (projectDirectory, options = {}) => {
|
|
138
|
+
const config = await getEventCatalogConfigFile(projectDirectory);
|
|
139
|
+
const sources = config.federation?.sources ?? [];
|
|
140
|
+
options.onProgress?.({ type: "configured", sources: sources.length });
|
|
141
|
+
if (sources.length === 0) {
|
|
142
|
+
await cleanupPreviousFederation(projectDirectory, options.onProgress);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (options.isFederationEnabled && !await options.isFederationEnabled()) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
"Cannot federate catalogs: EventCatalog federation is an Enterprise feature. Visit https://www.eventcatalog.dev/pricing to enable federation."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
validateSources(sources);
|
|
151
|
+
const rules = resolveFederationRules(config.federation?.rules);
|
|
152
|
+
if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
|
|
153
|
+
const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
|
|
154
|
+
const resolvedSources = [];
|
|
155
|
+
for (const [index, source] of sources.entries()) {
|
|
156
|
+
const current = index + 1;
|
|
157
|
+
options.onProgress?.({ type: "source:start", source, current, total: sources.length });
|
|
158
|
+
try {
|
|
159
|
+
const resolved = await provider.resolve(source);
|
|
160
|
+
resolvedSources.push({ config: source, resolved });
|
|
161
|
+
options.onProgress?.({
|
|
162
|
+
type: "source:complete",
|
|
163
|
+
source,
|
|
164
|
+
current,
|
|
165
|
+
total: sources.length,
|
|
166
|
+
commit: resolved.commit,
|
|
167
|
+
resources: resolved.index.resources.length,
|
|
168
|
+
generated: resolved.generated
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
throw new Error(`Failed to federate source "${source.id}": ${message}`, { cause: error });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const outDir = path.join(projectDirectory, "federated");
|
|
176
|
+
const lockPath = path.join(projectDirectory, "eventcatalog.lock");
|
|
177
|
+
const previousLock = await readLock(lockPath);
|
|
178
|
+
const resources = resolvedSources.reduce((total, source) => total + source.resolved.index.resources.length, 0);
|
|
179
|
+
const remoteIndexes = resolvedSources.map(({ resolved }) => resolved.index);
|
|
180
|
+
options.onProgress?.({ type: "local:start" });
|
|
181
|
+
const localIndex = await createSDK(projectDirectory).buildIndex({
|
|
182
|
+
source: config.cId,
|
|
183
|
+
commit: "local",
|
|
184
|
+
hashContent: false,
|
|
185
|
+
includeFederated: false
|
|
186
|
+
});
|
|
187
|
+
options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
|
|
188
|
+
options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
|
|
189
|
+
const ownershipGraph = resolve([localIndex, ...remoteIndexes]);
|
|
190
|
+
const graph = resolve(remoteIndexes);
|
|
191
|
+
const diagnosticGraph = createDiagnosticGraph(
|
|
192
|
+
graph,
|
|
193
|
+
ownershipGraph,
|
|
194
|
+
new Set(remoteIndexes.map((index) => index.source)),
|
|
195
|
+
localIndex.source
|
|
196
|
+
);
|
|
197
|
+
const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
|
|
198
|
+
options.onProgress?.({ type: "resolved", graph, diagnostics });
|
|
199
|
+
const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
|
|
200
|
+
if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
|
|
201
|
+
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
202
|
+
if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
|
|
203
|
+
const publicPaths = [
|
|
204
|
+
...Object.keys(previousLock?.publicFiles ?? {}),
|
|
205
|
+
...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
|
|
206
|
+
];
|
|
207
|
+
const { hydrateResult, publicResult } = await withFederationOutputTransaction(
|
|
208
|
+
projectDirectory,
|
|
209
|
+
outDir,
|
|
210
|
+
publicPaths,
|
|
211
|
+
async () => {
|
|
212
|
+
const sourcesById = new Map(sources.map((source) => [source.id, source]));
|
|
213
|
+
options.onProgress?.({ type: "hydrating", outDir });
|
|
214
|
+
let hydratedFiles = 0;
|
|
215
|
+
let cachedFiles = 0;
|
|
216
|
+
const hydrateResult2 = await hydrate(graph, {
|
|
217
|
+
outDir,
|
|
218
|
+
cache: createFederationContentCache(projectDirectory, {
|
|
219
|
+
read: options.useCache !== false,
|
|
220
|
+
onHit: () => {
|
|
221
|
+
cachedFiles += 1;
|
|
222
|
+
options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
|
|
223
|
+
}
|
|
224
|
+
}),
|
|
225
|
+
fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
|
|
226
|
+
const source = sourcesById.get(sourceId);
|
|
227
|
+
if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
|
|
228
|
+
const content = await provider.fetchContent({ source, commit, path: artifactPath });
|
|
229
|
+
hydratedFiles += 1;
|
|
230
|
+
options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
|
|
231
|
+
return content;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
const publicResult2 = await composePublicAssets({
|
|
235
|
+
projectDirectory,
|
|
236
|
+
federatedDirectory: outDir,
|
|
237
|
+
assets: graph.assets,
|
|
238
|
+
previousFiles: previousLock?.publicFiles,
|
|
239
|
+
collisionPaths: new Set(
|
|
240
|
+
graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
|
|
241
|
+
)
|
|
242
|
+
});
|
|
243
|
+
options.onProgress?.({ type: "public:complete", result: publicResult2 });
|
|
244
|
+
const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
245
|
+
await writeLock(lockPath, {
|
|
246
|
+
lockVersion: 1,
|
|
247
|
+
sources: resolvedSources.map(({ config: source, resolved }) => ({
|
|
248
|
+
id: source.id,
|
|
249
|
+
digest: `sha256:${createHash("sha256").update(resolved.bytes).digest("hex")}`,
|
|
250
|
+
commit: resolved.commit,
|
|
251
|
+
resolvedAt
|
|
252
|
+
})).sort((left, right) => left.id.localeCompare(right.id)),
|
|
253
|
+
publicFiles: publicResult2.files
|
|
254
|
+
});
|
|
255
|
+
return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
const result = {
|
|
259
|
+
sources: sources.length,
|
|
260
|
+
resources,
|
|
261
|
+
graph,
|
|
262
|
+
diagnostics,
|
|
263
|
+
hydrate: hydrateResult,
|
|
264
|
+
public: publicResult,
|
|
265
|
+
outDir,
|
|
266
|
+
lockPath
|
|
267
|
+
};
|
|
268
|
+
options.onProgress?.({ type: "complete", result });
|
|
269
|
+
return result;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export {
|
|
273
|
+
FederationConflictError,
|
|
274
|
+
FederationDiagnosticError,
|
|
275
|
+
federateCatalog
|
|
276
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/federation/diagnostics.ts
|
|
2
|
+
var federationRuleDefaults = {
|
|
3
|
+
"federation/duplicate-source": "error",
|
|
4
|
+
"federation/type-collision": "error",
|
|
5
|
+
"federation/pointer-type-mismatch": "error",
|
|
6
|
+
"federation/facet-disagreement": "error",
|
|
7
|
+
"federation/asset-collision": "warn",
|
|
8
|
+
"federation/missing-resource": "warn",
|
|
9
|
+
"federation/unresolved-version": "warn"
|
|
10
|
+
};
|
|
11
|
+
var resolveFederationRules = (rules = {}) => {
|
|
12
|
+
for (const [rule, level] of Object.entries(rules)) {
|
|
13
|
+
if (!Object.hasOwn(federationRuleDefaults, rule)) throw new Error(`Unknown federation rule "${rule}".`);
|
|
14
|
+
if (level !== "off" && level !== "warn" && level !== "error") {
|
|
15
|
+
throw new Error(`Invalid level "${String(level)}" for federation rule "${rule}". Expected off, warn, or error.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { ...federationRuleDefaults, ...rules };
|
|
19
|
+
};
|
|
20
|
+
var getExternalResource = (external) => `${external.id}${external.version === void 0 ? "" : `@${external.version}`}`;
|
|
21
|
+
var getVersionedResource = (id, version) => `${id}${version == null ? "" : `@${version}`}`;
|
|
22
|
+
var getDocumentedResourceType = (type) => {
|
|
23
|
+
const usesAn = type === "adr" || type === "agent" || type === "entity" || type === "event";
|
|
24
|
+
return `documented as ${usesAn ? "an" : "a"} ${type}`;
|
|
25
|
+
};
|
|
26
|
+
var getConflictDiagnostic = (conflict, graph) => {
|
|
27
|
+
const catalogs = { label: "catalogs", value: conflict.sources.join(", ") };
|
|
28
|
+
switch (conflict.kind) {
|
|
29
|
+
case "duplicate-source":
|
|
30
|
+
return {
|
|
31
|
+
severity: "error",
|
|
32
|
+
message: "Resource has multiple owners",
|
|
33
|
+
rule: "federation/duplicate-source",
|
|
34
|
+
attributes: [
|
|
35
|
+
{ label: "resource", value: conflict.id },
|
|
36
|
+
catalogs,
|
|
37
|
+
{ label: "resolution", value: "assign a single owning catalog" }
|
|
38
|
+
]
|
|
39
|
+
};
|
|
40
|
+
case "type-collision": {
|
|
41
|
+
const types = [
|
|
42
|
+
...new Map(
|
|
43
|
+
graph.entities.filter((entity) => entity.id === conflict.id).map(
|
|
44
|
+
(entity) => [
|
|
45
|
+
`${entity.resolvedFrom.source}:${entity.type}`,
|
|
46
|
+
{ label: entity.resolvedFrom.source, value: getDocumentedResourceType(entity.type) }
|
|
47
|
+
]
|
|
48
|
+
)
|
|
49
|
+
).values()
|
|
50
|
+
];
|
|
51
|
+
return {
|
|
52
|
+
severity: "error",
|
|
53
|
+
message: "Resource ID has conflicting types",
|
|
54
|
+
rule: "federation/type-collision",
|
|
55
|
+
attributes: [{ label: "resource", value: conflict.id }, ...types.length > 0 ? types : [catalogs]]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
case "pointer-type-mismatch": {
|
|
59
|
+
const typeMismatch = /^Expected (.+) but found (.+)$/.exec(conflict.detail ?? "");
|
|
60
|
+
return {
|
|
61
|
+
severity: "error",
|
|
62
|
+
message: "Reference type does not match resource",
|
|
63
|
+
rule: "federation/pointer-type-mismatch",
|
|
64
|
+
attributes: [
|
|
65
|
+
{ label: "resource", value: conflict.id },
|
|
66
|
+
...typeMismatch ? [
|
|
67
|
+
{ label: "expected type", value: typeMismatch[1] },
|
|
68
|
+
{ label: "actual type", value: typeMismatch[2] }
|
|
69
|
+
] : conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
|
|
70
|
+
catalogs
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
case "facet-disagreement":
|
|
75
|
+
return {
|
|
76
|
+
severity: "error",
|
|
77
|
+
message: "Catalogs disagree about this resource",
|
|
78
|
+
rule: "federation/facet-disagreement",
|
|
79
|
+
attributes: [
|
|
80
|
+
{ label: "resource", value: conflict.id },
|
|
81
|
+
...conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
|
|
82
|
+
catalogs
|
|
83
|
+
]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var getFederationDiagnostics = (graph, rules = {}) => {
|
|
88
|
+
const resolvedRules = resolveFederationRules(rules);
|
|
89
|
+
const diagnostics = graph.conflicts.map((conflict) => getConflictDiagnostic(conflict, graph));
|
|
90
|
+
for (const external of graph.externals) {
|
|
91
|
+
const resource = getExternalResource(external);
|
|
92
|
+
for (const referencedBy of external.referencedBy) {
|
|
93
|
+
const referrer = graph.entities.find((entity) => entity.id === referencedBy);
|
|
94
|
+
diagnostics.push({
|
|
95
|
+
severity: "warning",
|
|
96
|
+
message: "Referenced EventCatalog resource does not exist",
|
|
97
|
+
rule: "federation/missing-resource",
|
|
98
|
+
attributes: [
|
|
99
|
+
{ label: "source catalog", value: referrer?.resolvedFrom.source ?? "unknown" },
|
|
100
|
+
{ label: "referenced by", value: referencedBy },
|
|
101
|
+
{ label: "missing resource", value: resource }
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const edge of graph.edges.filter((edge2) => edge2.status === "unresolved")) {
|
|
107
|
+
const availableVersions = [
|
|
108
|
+
...new Set(graph.entities.filter((entity) => entity.id === edge.to).map((entity) => entity.version ?? "unversioned"))
|
|
109
|
+
].sort((left, right) => left.localeCompare(right, void 0, { numeric: true }));
|
|
110
|
+
diagnostics.push({
|
|
111
|
+
severity: "warning",
|
|
112
|
+
message: "Referenced EventCatalog resource version does not exist",
|
|
113
|
+
rule: "federation/unresolved-version",
|
|
114
|
+
attributes: [
|
|
115
|
+
{ label: "source catalog", value: edge.fromResolvedFrom.source },
|
|
116
|
+
{ label: "referenced by", value: getVersionedResource(edge.from, edge.fromVersion) },
|
|
117
|
+
{ label: "resource", value: edge.to },
|
|
118
|
+
{ label: "requested version", value: edge.pointer ?? "unspecified" },
|
|
119
|
+
{ label: "available versions", value: availableVersions.join(", ") }
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const warning of graph.warnings) {
|
|
124
|
+
diagnostics.push({
|
|
125
|
+
severity: "warning",
|
|
126
|
+
message: "Asset collision",
|
|
127
|
+
rule: "federation/asset-collision",
|
|
128
|
+
attributes: [
|
|
129
|
+
{ label: "asset", value: warning.path },
|
|
130
|
+
{ label: "sources", value: warning.sources.join(", ") },
|
|
131
|
+
{ label: "winner", value: warning.winner },
|
|
132
|
+
{ label: "resolution", value: "last configured source wins" }
|
|
133
|
+
]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return diagnostics.flatMap((diagnostic) => {
|
|
137
|
+
const level = resolvedRules[diagnostic.rule];
|
|
138
|
+
if (level === "off") return [];
|
|
139
|
+
return [{ ...diagnostic, severity: level === "error" ? "error" : "warning" }];
|
|
140
|
+
}).sort(
|
|
141
|
+
(left, right) => (left.severity === right.severity ? 0 : left.severity === "error" ? -1 : 1) || left.rule.localeCompare(right.rule) || (left.attributes[0]?.value ?? "").localeCompare(right.attributes[0]?.value ?? "")
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
var getFederationDiagnosticCounts = (diagnostics) => diagnostics.reduce(
|
|
145
|
+
(counts, diagnostic) => ({
|
|
146
|
+
errors: counts.errors + (diagnostic.severity === "error" ? 1 : 0),
|
|
147
|
+
warnings: counts.warnings + (diagnostic.severity === "warning" ? 1 : 0)
|
|
148
|
+
}),
|
|
149
|
+
{ errors: 0, warnings: 0 }
|
|
150
|
+
);
|
|
151
|
+
var getVisibleFederationDiagnostics = (diagnostics, verbose) => diagnostics.filter((diagnostic) => verbose || diagnostic.severity === "error");
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
federationRuleDefaults,
|
|
155
|
+
resolveFederationRules,
|
|
156
|
+
getFederationDiagnostics,
|
|
157
|
+
getFederationDiagnosticCounts,
|
|
158
|
+
getVisibleFederationDiagnostics
|
|
159
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZBPMCEXS.js";
|
|
4
4
|
|
|
5
5
|
// src/utils/cli-logger.ts
|
|
6
6
|
import pc from "picocolors";
|
|
@@ -37,6 +37,29 @@ var logger = {
|
|
|
37
37
|
warning: (message, tag = "warn") => {
|
|
38
38
|
console.log(formatMessage(tag, message, pc.yellow));
|
|
39
39
|
},
|
|
40
|
+
line: (message = "") => {
|
|
41
|
+
console.log(message);
|
|
42
|
+
},
|
|
43
|
+
diagnostic: (severity, message, rule, attributes = []) => {
|
|
44
|
+
const isError = severity === "error";
|
|
45
|
+
const color = isError ? pc.red : pc.yellow;
|
|
46
|
+
const icon = isError ? "\u2716" : "\u26A0";
|
|
47
|
+
const label = `${icon} ${severity.padEnd(7)} ${message}`;
|
|
48
|
+
const ruleSpacing = " ".repeat(Math.max(2, 72 - label.length));
|
|
49
|
+
console.log(` ${color(icon)} ${color(severity.padEnd(7))} ${message}${ruleSpacing}${pc.gray(rule)}`);
|
|
50
|
+
for (const attribute of attributes) {
|
|
51
|
+
console.log(` - ${pc.dim(`${attribute.label}:`)} ${attribute.value}`);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
diagnosticSummary: (errors, warnings) => {
|
|
55
|
+
const total = errors + warnings;
|
|
56
|
+
const color = errors > 0 ? pc.red : pc.yellow;
|
|
57
|
+
const icon = errors > 0 ? "\u2716" : "\u26A0";
|
|
58
|
+
const problems = `${total} problem${total === 1 ? "" : "s"}`;
|
|
59
|
+
const errorCount = `${errors} error${errors === 1 ? "" : "s"}`;
|
|
60
|
+
const warningCount = `${warnings} warning${warnings === 1 ? "" : "s"}`;
|
|
61
|
+
console.log(color(`${icon} ${problems} (${errorCount}, ${warningCount})`));
|
|
62
|
+
},
|
|
40
63
|
dim: (message) => {
|
|
41
64
|
console.log(pc.dim(message));
|
|
42
65
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/federation/filesystem-source-provider.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import createSDK from "@eventcatalog/sdk";
|
|
6
|
+
var FILESYSTEM_SOURCE_PREFIX = "file:";
|
|
7
|
+
var isWithinDirectory = (directory, target) => {
|
|
8
|
+
const relativePath = path.relative(directory, target);
|
|
9
|
+
return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
|
|
10
|
+
};
|
|
11
|
+
var assertPortableRelativePath = (filePath, label, allowRoot = true) => {
|
|
12
|
+
const portablePath = filePath.replaceAll("\\", "/");
|
|
13
|
+
const normalizedPath = path.posix.normalize(portablePath);
|
|
14
|
+
const isUnsafe = filePath.includes("\\") || filePath.includes("\0") || path.posix.isAbsolute(normalizedPath) || /^[a-zA-Z]:\//.test(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || !allowRoot && (normalizedPath === "." || normalizedPath === "");
|
|
15
|
+
if (isUnsafe) throw new Error(`${label} "${filePath}" escapes its filesystem source`);
|
|
16
|
+
return normalizedPath;
|
|
17
|
+
};
|
|
18
|
+
var getSourceRoot = (projectDirectory, source) => {
|
|
19
|
+
if (!source.source.startsWith(FILESYSTEM_SOURCE_PREFIX)) {
|
|
20
|
+
throw new Error(`Unsupported federation source "${source.source}". Expected file:path/to/catalog.`);
|
|
21
|
+
}
|
|
22
|
+
const locator = source.source.slice(FILESYSTEM_SOURCE_PREFIX.length);
|
|
23
|
+
if (!locator.trim()) throw new Error(`Filesystem federation source "${source.id}" requires a path after "file:".`);
|
|
24
|
+
return path.resolve(projectDirectory, locator);
|
|
25
|
+
};
|
|
26
|
+
var getCatalogDirectory = async (projectDirectory, source) => {
|
|
27
|
+
if (source.ref) throw new Error(`Filesystem federation source "${source.id}" does not support "ref".`);
|
|
28
|
+
const sourceRoot = getSourceRoot(projectDirectory, source);
|
|
29
|
+
const catalogPath = assertPortableRelativePath(source.path ?? ".", "Catalog path");
|
|
30
|
+
const catalogDirectory = path.resolve(sourceRoot, ...catalogPath.split("/"));
|
|
31
|
+
if (!isWithinDirectory(sourceRoot, catalogDirectory)) {
|
|
32
|
+
throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const [realSourceRoot, realCatalogDirectory] = await Promise.all([fs.realpath(sourceRoot), fs.realpath(catalogDirectory)]);
|
|
36
|
+
if (!isWithinDirectory(realSourceRoot, realCatalogDirectory)) {
|
|
37
|
+
throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
|
|
38
|
+
}
|
|
39
|
+
if (!(await fs.stat(realCatalogDirectory)).isDirectory()) {
|
|
40
|
+
throw new Error(`Filesystem federation source "${source.id}" is not a directory: ${catalogDirectory}`);
|
|
41
|
+
}
|
|
42
|
+
return realCatalogDirectory;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code === "ENOENT") {
|
|
45
|
+
throw new Error(`Filesystem federation source "${source.id}" does not exist: ${catalogDirectory}`, { cause: error });
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var getArtifactPath = async (catalogDirectory, source, artifactPath) => {
|
|
51
|
+
const normalizedPath = assertPortableRelativePath(artifactPath, "Federated artifact path", false);
|
|
52
|
+
const filePath = path.resolve(catalogDirectory, ...normalizedPath.split("/"));
|
|
53
|
+
if (!isWithinDirectory(catalogDirectory, filePath)) {
|
|
54
|
+
throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const realFilePath = await fs.realpath(filePath);
|
|
58
|
+
if (!isWithinDirectory(catalogDirectory, realFilePath)) {
|
|
59
|
+
throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
|
|
60
|
+
}
|
|
61
|
+
return realFilePath;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === "ENOENT") {
|
|
64
|
+
throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`, { cause: error });
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var createFileSystemSourceProvider = (projectDirectory) => ({
|
|
70
|
+
async resolve(source) {
|
|
71
|
+
const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
|
|
72
|
+
const localIndex = await createSDK(catalogDirectory).buildIndex({
|
|
73
|
+
source: source.id,
|
|
74
|
+
commit: "local",
|
|
75
|
+
includeFederated: false
|
|
76
|
+
});
|
|
77
|
+
const snapshot = createHash("sha256").update(JSON.stringify(localIndex)).digest("hex").slice(0, 12);
|
|
78
|
+
const index = { ...localIndex, commit: `local:${snapshot}` };
|
|
79
|
+
const bytes = Buffer.from(JSON.stringify(index));
|
|
80
|
+
return { bytes, index, commit: index.commit, generated: true };
|
|
81
|
+
},
|
|
82
|
+
async fetchContent({ source, path: artifactPath }) {
|
|
83
|
+
const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
|
|
84
|
+
return fs.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
createFileSystemSourceProvider
|
|
90
|
+
};
|