@eventcatalog/core 3.26.9 → 3.26.11
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-7CRQXVYN.js → chunk-24RZHNNP.js} +1 -1
- package/dist/{chunk-7BSZQI3M.js → chunk-B44ZJW35.js} +1 -1
- package/dist/{chunk-U6H6DNUN.js → chunk-PHJNSP6V.js} +1 -1
- package/dist/{chunk-XRWNLEVD.js → chunk-XP2QYGFD.js} +1 -1
- package/dist/{chunk-NL4AAHR6.js → chunk-ZWJOF3OY.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +8 -5
- package/dist/eventcatalog.js +12 -9
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +1 -1
- package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +5 -3
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +2 -0
- package/eventcatalog/src/content.config.ts +3 -16
- package/eventcatalog/src/enterprise/LICENSE +57 -0
- package/eventcatalog/src/enterprise/ai/chat-api.ts +5 -0
- package/eventcatalog/src/enterprise/analytics/adapters/ga4.ts +5 -0
- package/eventcatalog/src/enterprise/analytics/adapters/gtm.ts +5 -0
- package/eventcatalog/src/enterprise/analytics/adapters/posthog.ts +5 -0
- package/eventcatalog/src/enterprise/analytics/tracker.ts +5 -0
- package/eventcatalog/src/enterprise/api/catalog.ts +5 -0
- package/eventcatalog/src/{pages → enterprise}/api/schemas/[collection]/[id]/[version]/index.ts +5 -0
- package/eventcatalog/src/{pages → enterprise}/api/schemas/services/[id]/[version]/[specification]/index.ts +5 -0
- package/eventcatalog/src/enterprise/auth/[...auth].ts +5 -0
- package/eventcatalog/src/enterprise/auth/middleware/middleware-auth.ts +5 -0
- package/eventcatalog/src/enterprise/auth/middleware/middleware.ts +5 -0
- package/eventcatalog/src/enterprise/collections/custom-pages.ts +5 -0
- package/eventcatalog/src/enterprise/collections/index.ts +6 -0
- package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +647 -0
- package/eventcatalog/src/enterprise/collections/resource-docs.ts +24 -0
- package/eventcatalog/src/enterprise/custom-documentation/collection.ts +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/CustomDocsNavWrapper.tsx +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NestedItem.tsx +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/components/NoResultsFound.tsx +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/types.ts +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/utils/badge-styles.ts +5 -0
- package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +5 -0
- package/eventcatalog/src/enterprise/feature.ts +68 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +61 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.ts +14 -6
- package/eventcatalog/src/enterprise/fields/field-indexer.ts +5 -0
- package/eventcatalog/src/enterprise/fields/fields-db.test.ts +5 -0
- package/eventcatalog/src/enterprise/fields/fields-db.ts +5 -0
- package/eventcatalog/src/enterprise/fields/pages/api/fields.ts +5 -0
- package/eventcatalog/{integrations → src/enterprise/integrations}/eventcatalog-features.ts +48 -2
- package/eventcatalog/src/enterprise/mcp/mcp-server.ts +5 -0
- package/eventcatalog/src/enterprise/print/_message.data.ts +5 -0
- package/eventcatalog/src/enterprise/print/components/PrintSchemaViewer.tsx +5 -0
- package/eventcatalog/src/enterprise/print/utils.ts +5 -0
- package/eventcatalog/src/enterprise/tools/catalog-tools.ts +5 -0
- package/eventcatalog/src/enterprise/tools/index.ts +5 -0
- package/eventcatalog/src/enterprise/visualizer-layout/reset.ts +5 -0
- package/eventcatalog/src/enterprise/visualizer-layout/save.ts +5 -0
- package/eventcatalog/src/utils/collections/resource-docs.ts +13 -642
- package/eventcatalog/src/utils/feature.ts +27 -75
- package/package.json +2 -1
- package/LICENSE.md +0 -30
- package/eventcatalog/src/enterprise/LICENSE.txt +0 -4
- /package/eventcatalog/src/{pages → enterprise/custom-documentation/pages}/docs/custom/[...path]/_index.data.ts +0 -0
- /package/eventcatalog/src/{pages → enterprise/custom-documentation/pages}/docs/custom/[...path]/index.astro +0 -0
- /package/eventcatalog/src/{pages → enterprise/custom-documentation/pages}/docs/custom/[...path].mdx.ts +0 -0
- /package/eventcatalog/src/{pages → enterprise/custom-documentation/pages}/docs/custom/feature.astro +0 -0
- /package/eventcatalog/src/{pages/docs/custom/index.astro → enterprise/custom-documentation/pages/docs/custom/root-index.astro} +0 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the EventCatalog Commercial License.
|
|
3
|
+
* See /packages/core/eventcatalog/src/enterprise/LICENSE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { coerce, rcompare } from 'semver';
|
|
9
|
+
import { sortVersioned } from '../../utils/collections/util';
|
|
10
|
+
import { isResourceDocsEnabled } from '../feature';
|
|
11
|
+
|
|
12
|
+
const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
|
|
13
|
+
|
|
14
|
+
export type ResourceCollection =
|
|
15
|
+
| 'domains'
|
|
16
|
+
| 'services'
|
|
17
|
+
| 'events'
|
|
18
|
+
| 'commands'
|
|
19
|
+
| 'queries'
|
|
20
|
+
| 'flows'
|
|
21
|
+
| 'containers'
|
|
22
|
+
| 'channels'
|
|
23
|
+
| 'entities'
|
|
24
|
+
| 'data-products';
|
|
25
|
+
|
|
26
|
+
type BaseResourceDocEntry = CollectionEntry<'resourceDocs'>;
|
|
27
|
+
type BaseResourceDocCategoryEntry = CollectionEntry<'resourceDocCategories'>;
|
|
28
|
+
|
|
29
|
+
export type ResourceDocEntry = Omit<BaseResourceDocEntry, 'data'> & {
|
|
30
|
+
data: BaseResourceDocEntry['data'] & {
|
|
31
|
+
id: string;
|
|
32
|
+
type: string;
|
|
33
|
+
version: string;
|
|
34
|
+
order?: number;
|
|
35
|
+
resourceCollection: ResourceCollection;
|
|
36
|
+
resourceId: string;
|
|
37
|
+
resourceVersion: string;
|
|
38
|
+
versions: string[];
|
|
39
|
+
latestVersion: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ResourceDocCategoryEntry = Omit<BaseResourceDocCategoryEntry, 'data'> & {
|
|
44
|
+
data: BaseResourceDocCategoryEntry['data'] & {
|
|
45
|
+
type: string;
|
|
46
|
+
resourceCollection: ResourceCollection;
|
|
47
|
+
resourceId: string;
|
|
48
|
+
resourceVersion: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ResourceDocGroup = {
|
|
53
|
+
type: string;
|
|
54
|
+
docs: ResourceDocEntry[];
|
|
55
|
+
label?: string;
|
|
56
|
+
position?: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type ResourceLookup = {
|
|
60
|
+
latestById: Map<string, string>;
|
|
61
|
+
versionsById: Map<string, Set<string>>;
|
|
62
|
+
folderToId: Map<string, string>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type InferredResource = {
|
|
66
|
+
resourceCollection: ResourceCollection;
|
|
67
|
+
resourceId: string;
|
|
68
|
+
resourceVersion: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
let memoryCache: ResourceDocEntry[] | null = null;
|
|
72
|
+
let memoryCategoryCache: ResourceDocCategoryEntry[] | null = null;
|
|
73
|
+
let memoryResourceLookupCache: Record<ResourceCollection, ResourceLookup> | null = null;
|
|
74
|
+
let memoryResourceLookupPromise: Promise<Record<ResourceCollection, ResourceLookup>> | null = null;
|
|
75
|
+
|
|
76
|
+
const normalizePath = (value: string) => value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
77
|
+
const normalizeTypeName = (value: string) => value.trim().toLowerCase();
|
|
78
|
+
|
|
79
|
+
const inferOrderFromFilePath = (filePath: string): number | undefined => {
|
|
80
|
+
const normalizedPath = normalizePath(filePath);
|
|
81
|
+
const fileName = normalizedPath.split('/').pop();
|
|
82
|
+
|
|
83
|
+
if (!fileName) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fileNameWithoutExtension = fileName.replace(/\.(md|mdx)$/i, '');
|
|
88
|
+
const orderMatch = fileNameWithoutExtension.match(/^(\d+)(?:[-_.\s]|$)/);
|
|
89
|
+
|
|
90
|
+
if (!orderMatch) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsedOrder = Number.parseInt(orderMatch[1], 10);
|
|
95
|
+
return Number.isFinite(parsedOrder) ? parsedOrder : undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const inferDocIdFromFilePath = (filePath: string): string | undefined => {
|
|
99
|
+
const normalizedPath = normalizePath(filePath);
|
|
100
|
+
const fileName = normalizedPath.split('/').pop();
|
|
101
|
+
|
|
102
|
+
if (!fileName) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const fileNameWithoutExtension = fileName.replace(/\.(md|mdx)$/i, '');
|
|
107
|
+
if (!fileNameWithoutExtension) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Support ordered filenames like "01-my-doc" while keeping stable ids.
|
|
112
|
+
const idWithoutNumericPrefix = fileNameWithoutExtension.replace(/^\d+(?:[-_.\s]+)?/, '');
|
|
113
|
+
return idWithoutNumericPrefix || fileNameWithoutExtension;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const inferDocTypeFromFilePath = (filePath: string): string | undefined => {
|
|
117
|
+
const normalizedPath = normalizePath(filePath);
|
|
118
|
+
const docsMarker = '/docs/';
|
|
119
|
+
const docsIndex = normalizedPath.indexOf(docsMarker);
|
|
120
|
+
|
|
121
|
+
if (docsIndex === -1) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const docsRelativePath = normalizedPath.slice(docsIndex + docsMarker.length);
|
|
126
|
+
const segments = docsRelativePath.split('/').filter(Boolean);
|
|
127
|
+
const firstSegment = segments[0];
|
|
128
|
+
|
|
129
|
+
if (!firstSegment || firstSegment === 'versioned') {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (/\.[a-z0-9]+$/i.test(firstSegment)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return firstSegment;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getCategoryFilePriority = (filePath: string): number => {
|
|
141
|
+
const fileName = normalizePath(filePath).split('/').pop()?.toLowerCase();
|
|
142
|
+
if (fileName === 'category.json') {
|
|
143
|
+
return 2;
|
|
144
|
+
}
|
|
145
|
+
if (fileName === '_category_.json') {
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const inferResourceFromFilePath = (filePath: string): InferredResource | null => {
|
|
152
|
+
const normalizedPath = normalizePath(filePath);
|
|
153
|
+
const docsIndex = normalizedPath.indexOf('/docs/');
|
|
154
|
+
|
|
155
|
+
if (docsIndex === -1) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const resourcePath = normalizedPath.slice(0, docsIndex);
|
|
160
|
+
const segments = resourcePath.split('/').filter(Boolean);
|
|
161
|
+
|
|
162
|
+
const parseVersion = (index: number) => {
|
|
163
|
+
if (segments[index + 2] === 'versioned' && segments[index + 3]) {
|
|
164
|
+
return segments[index + 3];
|
|
165
|
+
}
|
|
166
|
+
return 'latest';
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const segmentMappings: Array<{ segment: string; collection: ResourceCollection }> = [
|
|
170
|
+
{ segment: 'events', collection: 'events' },
|
|
171
|
+
{ segment: 'commands', collection: 'commands' },
|
|
172
|
+
{ segment: 'queries', collection: 'queries' },
|
|
173
|
+
{ segment: 'services', collection: 'services' },
|
|
174
|
+
{ segment: 'domains', collection: 'domains' },
|
|
175
|
+
{ segment: 'subdomains', collection: 'domains' },
|
|
176
|
+
{ segment: 'flows', collection: 'flows' },
|
|
177
|
+
{ segment: 'containers', collection: 'containers' },
|
|
178
|
+
{ segment: 'channels', collection: 'channels' },
|
|
179
|
+
{ segment: 'entities', collection: 'entities' },
|
|
180
|
+
{ segment: 'data-products', collection: 'data-products' },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
let matched: { index: number; collection: ResourceCollection } | null = null;
|
|
184
|
+
|
|
185
|
+
for (let index = 0; index < segments.length; index++) {
|
|
186
|
+
const match = segmentMappings.find((mapping) => mapping.segment === segments[index]);
|
|
187
|
+
if (!match) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!segments[index + 1]) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!matched || index > matched.index) {
|
|
196
|
+
matched = { index, collection: match.collection };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!matched) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
resourceCollection: matched.collection,
|
|
206
|
+
resourceId: segments[matched.index + 1],
|
|
207
|
+
resourceVersion: parseVersion(matched.index),
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const getResourceFolderFromFilePath = (filePath: string): string | undefined => {
|
|
212
|
+
const normalized = normalizePath(filePath);
|
|
213
|
+
const versionedIndex = normalized.indexOf('/versioned/');
|
|
214
|
+
if (versionedIndex !== -1) {
|
|
215
|
+
// e.g. "domains/BoxOffice/versioned/1.0.0/index.mdx" → "domains/BoxOffice"
|
|
216
|
+
const beforeVersioned = normalized.slice(0, versionedIndex);
|
|
217
|
+
return path.basename(beforeVersioned) || undefined;
|
|
218
|
+
}
|
|
219
|
+
// e.g. "domains/BoxOffice/index.mdx" → "BoxOffice"
|
|
220
|
+
const dir = path.dirname(normalized);
|
|
221
|
+
return dir ? path.basename(dir) : undefined;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const buildLookup = (
|
|
225
|
+
resources: Array<{
|
|
226
|
+
data: { id: string; version: string; hidden?: boolean };
|
|
227
|
+
filePath?: string;
|
|
228
|
+
}>
|
|
229
|
+
): ResourceLookup => {
|
|
230
|
+
const groupedById = new Map<string, string[]>();
|
|
231
|
+
const folderToId = new Map<string, string>();
|
|
232
|
+
|
|
233
|
+
for (const resource of resources) {
|
|
234
|
+
if (resource.data.hidden === true) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const list = groupedById.get(resource.data.id) || [];
|
|
238
|
+
list.push(resource.data.version);
|
|
239
|
+
groupedById.set(resource.data.id, list);
|
|
240
|
+
|
|
241
|
+
if (resource.filePath) {
|
|
242
|
+
const folderName = getResourceFolderFromFilePath(resource.filePath);
|
|
243
|
+
if (folderName && folderName !== resource.data.id) {
|
|
244
|
+
folderToId.set(folderName, resource.data.id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const latestById = new Map<string, string>();
|
|
250
|
+
const versionsById = new Map<string, Set<string>>();
|
|
251
|
+
|
|
252
|
+
for (const [id, versions] of groupedById.entries()) {
|
|
253
|
+
const sorted = sortVersioned([...new Set(versions)], (version) => version);
|
|
254
|
+
if (sorted.length > 0) {
|
|
255
|
+
latestById.set(id, sorted[0]);
|
|
256
|
+
versionsById.set(id, new Set(sorted));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { latestById, versionsById, folderToId };
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const getResourceLookups = async (): Promise<Record<ResourceCollection, ResourceLookup>> => {
|
|
264
|
+
if (CACHE_ENABLED && memoryResourceLookupCache) {
|
|
265
|
+
return memoryResourceLookupCache;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (CACHE_ENABLED && memoryResourceLookupPromise) {
|
|
269
|
+
return memoryResourceLookupPromise;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lookupPromise = (async () => {
|
|
273
|
+
const [domains, services, events, commands, queries, flows, containers, channels, entities, dataProducts] = await Promise.all(
|
|
274
|
+
[
|
|
275
|
+
getCollection('domains'),
|
|
276
|
+
getCollection('services'),
|
|
277
|
+
getCollection('events'),
|
|
278
|
+
getCollection('commands'),
|
|
279
|
+
getCollection('queries'),
|
|
280
|
+
getCollection('flows'),
|
|
281
|
+
getCollection('containers'),
|
|
282
|
+
getCollection('channels'),
|
|
283
|
+
getCollection('entities'),
|
|
284
|
+
getCollection('data-products'),
|
|
285
|
+
]
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
domains: buildLookup(domains),
|
|
290
|
+
services: buildLookup(services),
|
|
291
|
+
events: buildLookup(events),
|
|
292
|
+
commands: buildLookup(commands),
|
|
293
|
+
queries: buildLookup(queries),
|
|
294
|
+
flows: buildLookup(flows),
|
|
295
|
+
containers: buildLookup(containers),
|
|
296
|
+
channels: buildLookup(channels),
|
|
297
|
+
entities: buildLookup(entities),
|
|
298
|
+
'data-products': buildLookup(dataProducts),
|
|
299
|
+
};
|
|
300
|
+
})();
|
|
301
|
+
|
|
302
|
+
if (CACHE_ENABLED) {
|
|
303
|
+
memoryResourceLookupPromise = lookupPromise;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const lookups = await lookupPromise;
|
|
308
|
+
if (CACHE_ENABLED) {
|
|
309
|
+
memoryResourceLookupCache = lookups;
|
|
310
|
+
}
|
|
311
|
+
return lookups;
|
|
312
|
+
} finally {
|
|
313
|
+
if (CACHE_ENABLED) {
|
|
314
|
+
memoryResourceLookupPromise = null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const resolveResourceFromPath = (
|
|
320
|
+
filePath: string,
|
|
321
|
+
lookups: Record<ResourceCollection, ResourceLookup>
|
|
322
|
+
): InferredResource | null => {
|
|
323
|
+
const inferredResource = inferResourceFromFilePath(filePath);
|
|
324
|
+
if (!inferredResource) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const { resourceCollection, resourceVersion } = inferredResource;
|
|
329
|
+
const lookup = lookups[resourceCollection];
|
|
330
|
+
|
|
331
|
+
// The inferred resourceId is the folder name from the file path. It may differ
|
|
332
|
+
// from the actual frontmatter id, so fall back to the folderToId mapping.
|
|
333
|
+
const folderName = inferredResource.resourceId;
|
|
334
|
+
const resolvedId = lookup.versionsById.has(folderName) ? folderName : (lookup.folderToId.get(folderName) ?? folderName);
|
|
335
|
+
|
|
336
|
+
const knownVersions = lookup.versionsById.get(resolvedId);
|
|
337
|
+
|
|
338
|
+
if (!knownVersions || knownVersions.size === 0) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const resolvedResourceVersion = resourceVersion === 'latest' ? (lookup.latestById.get(resolvedId) ?? null) : resourceVersion;
|
|
343
|
+
|
|
344
|
+
if (!resolvedResourceVersion || !knownVersions.has(resolvedResourceVersion)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
resourceCollection,
|
|
350
|
+
resourceId: resolvedId,
|
|
351
|
+
resourceVersion: resolvedResourceVersion,
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const getResourceDocs = async (): Promise<ResourceDocEntry[]> => {
|
|
356
|
+
if (!isResourceDocsEnabled()) {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (memoryCache && CACHE_ENABLED) {
|
|
361
|
+
return memoryCache;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const [docs, lookups] = await Promise.all([getCollection('resourceDocs'), getResourceLookups()]);
|
|
365
|
+
|
|
366
|
+
const docsWithResources = docs
|
|
367
|
+
.filter((doc) => doc.data.hidden !== true)
|
|
368
|
+
.map((doc) => {
|
|
369
|
+
if (!doc.filePath) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const resolvedResource = resolveResourceFromPath(doc.filePath, lookups);
|
|
374
|
+
if (!resolvedResource) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const inferredOrder = inferOrderFromFilePath(doc.filePath);
|
|
379
|
+
const resolvedOrder = typeof doc.data.order === 'number' ? doc.data.order : inferredOrder;
|
|
380
|
+
const inferredType = inferDocTypeFromFilePath(doc.filePath);
|
|
381
|
+
const resolvedType = doc.data.type || inferredType || 'pages';
|
|
382
|
+
const resolvedDocId = doc.data.id || inferDocIdFromFilePath(doc.filePath);
|
|
383
|
+
const { resourceCollection, resourceId, resourceVersion } = resolvedResource;
|
|
384
|
+
const resolvedDocVersion = doc.data.version || resourceVersion;
|
|
385
|
+
|
|
386
|
+
if (!resolvedDocId) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
...doc,
|
|
392
|
+
data: {
|
|
393
|
+
...doc.data,
|
|
394
|
+
id: resolvedDocId,
|
|
395
|
+
type: resolvedType,
|
|
396
|
+
version: resolvedDocVersion,
|
|
397
|
+
order: resolvedOrder,
|
|
398
|
+
resourceCollection,
|
|
399
|
+
resourceId,
|
|
400
|
+
resourceVersion,
|
|
401
|
+
versions: [],
|
|
402
|
+
latestVersion: resolvedDocVersion,
|
|
403
|
+
},
|
|
404
|
+
} as ResourceDocEntry;
|
|
405
|
+
})
|
|
406
|
+
.filter((doc): doc is ResourceDocEntry => doc !== null);
|
|
407
|
+
|
|
408
|
+
const docsByResourceAndId = new Map<string, ResourceDocEntry[]>();
|
|
409
|
+
|
|
410
|
+
for (const doc of docsWithResources) {
|
|
411
|
+
const key = `${doc.data.resourceCollection}:${doc.data.resourceId}:${doc.data.resourceVersion}:${doc.data.type}:${doc.data.id}`;
|
|
412
|
+
const versions = docsByResourceAndId.get(key) || [];
|
|
413
|
+
versions.push(doc);
|
|
414
|
+
docsByResourceAndId.set(key, versions);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const enrichedDocs: ResourceDocEntry[] = [];
|
|
418
|
+
|
|
419
|
+
for (const docsForResource of docsByResourceAndId.values()) {
|
|
420
|
+
const sortedByVersion = sortVersioned(docsForResource, (doc) => doc.data.version);
|
|
421
|
+
const allVersions = sortedByVersion.map((doc) => doc.data.version);
|
|
422
|
+
const latestVersion = allVersions[0];
|
|
423
|
+
|
|
424
|
+
for (const doc of sortedByVersion) {
|
|
425
|
+
enrichedDocs.push({
|
|
426
|
+
...doc,
|
|
427
|
+
data: {
|
|
428
|
+
...doc.data,
|
|
429
|
+
versions: allVersions,
|
|
430
|
+
latestVersion,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
enrichedDocs.sort((a, b) => {
|
|
437
|
+
const resourceKeyA = `${a.data.resourceCollection}:${a.data.resourceId}:${a.data.resourceVersion}`;
|
|
438
|
+
const resourceKeyB = `${b.data.resourceCollection}:${b.data.resourceId}:${b.data.resourceVersion}`;
|
|
439
|
+
if (resourceKeyA !== resourceKeyB) {
|
|
440
|
+
return resourceKeyA.localeCompare(resourceKeyB);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const typeCompare = a.data.type.localeCompare(b.data.type);
|
|
444
|
+
if (typeCompare !== 0) {
|
|
445
|
+
return typeCompare;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const titleA = (a.data.title || a.data.id).toLowerCase();
|
|
449
|
+
const titleB = (b.data.title || b.data.id).toLowerCase();
|
|
450
|
+
if (titleA !== titleB) {
|
|
451
|
+
return titleA.localeCompare(titleB);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const semverA = coerce(a.data.version);
|
|
455
|
+
const semverB = coerce(b.data.version);
|
|
456
|
+
if (semverA && semverB) {
|
|
457
|
+
return rcompare(semverA, semverB);
|
|
458
|
+
}
|
|
459
|
+
return b.data.version.localeCompare(a.data.version);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
memoryCache = enrichedDocs;
|
|
463
|
+
return enrichedDocs;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
export const getResourceDocCategories = async (): Promise<ResourceDocCategoryEntry[]> => {
|
|
467
|
+
if (!isResourceDocsEnabled()) {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (memoryCategoryCache && CACHE_ENABLED) {
|
|
472
|
+
return memoryCategoryCache;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const [categories, lookups] = await Promise.all([getCollection('resourceDocCategories'), getResourceLookups()]);
|
|
476
|
+
const categoriesByKey = new Map<string, ResourceDocCategoryEntry>();
|
|
477
|
+
|
|
478
|
+
for (const category of categories) {
|
|
479
|
+
if (!category.filePath) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const resolvedResource = resolveResourceFromPath(category.filePath, lookups);
|
|
484
|
+
if (!resolvedResource) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const categoryType = inferDocTypeFromFilePath(category.filePath) || 'pages';
|
|
489
|
+
const key = `${resolvedResource.resourceCollection}:${resolvedResource.resourceId}:${resolvedResource.resourceVersion}:${categoryType}`;
|
|
490
|
+
const existing = categoriesByKey.get(key);
|
|
491
|
+
|
|
492
|
+
const nextEntry: ResourceDocCategoryEntry = {
|
|
493
|
+
...category,
|
|
494
|
+
data: {
|
|
495
|
+
...category.data,
|
|
496
|
+
type: categoryType,
|
|
497
|
+
resourceCollection: resolvedResource.resourceCollection,
|
|
498
|
+
resourceId: resolvedResource.resourceId,
|
|
499
|
+
resourceVersion: resolvedResource.resourceVersion,
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
if (!existing) {
|
|
504
|
+
categoriesByKey.set(key, nextEntry);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const existingPriority = existing.filePath ? getCategoryFilePriority(existing.filePath) : 0;
|
|
509
|
+
const nextPriority = getCategoryFilePriority(category.filePath);
|
|
510
|
+
|
|
511
|
+
if (nextPriority >= existingPriority) {
|
|
512
|
+
if (
|
|
513
|
+
existing.filePath &&
|
|
514
|
+
category.filePath &&
|
|
515
|
+
getCategoryFilePriority(existing.filePath) !== getCategoryFilePriority(category.filePath)
|
|
516
|
+
) {
|
|
517
|
+
// Prefer category.json over _category_.json when both exist in the same folder.
|
|
518
|
+
console.warn(
|
|
519
|
+
`[resource-docs] Both category.json and _category_.json found for ${key}. Using ${
|
|
520
|
+
nextPriority > existingPriority ? category.filePath : existing.filePath
|
|
521
|
+
}.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
categoriesByKey.set(key, nextEntry);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const resolvedCategories = [...categoriesByKey.values()].sort((a, b) => {
|
|
529
|
+
const resourceKeyA = `${a.data.resourceCollection}:${a.data.resourceId}:${a.data.resourceVersion}`;
|
|
530
|
+
const resourceKeyB = `${b.data.resourceCollection}:${b.data.resourceId}:${b.data.resourceVersion}`;
|
|
531
|
+
if (resourceKeyA !== resourceKeyB) {
|
|
532
|
+
return resourceKeyA.localeCompare(resourceKeyB);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const positionA = typeof a.data.position === 'number' ? a.data.position : Number.POSITIVE_INFINITY;
|
|
536
|
+
const positionB = typeof b.data.position === 'number' ? b.data.position : Number.POSITIVE_INFINITY;
|
|
537
|
+
if (positionA !== positionB) {
|
|
538
|
+
return positionA - positionB;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return a.data.type.localeCompare(b.data.type);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
memoryCategoryCache = resolvedCategories;
|
|
545
|
+
return resolvedCategories;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
export const getResourceDocCategoriesForResource = async (
|
|
549
|
+
resourceCollection: ResourceCollection,
|
|
550
|
+
resourceId: string,
|
|
551
|
+
resourceVersion: string
|
|
552
|
+
): Promise<ResourceDocCategoryEntry[]> => {
|
|
553
|
+
const categories = await getResourceDocCategories();
|
|
554
|
+
return categories.filter(
|
|
555
|
+
(category) =>
|
|
556
|
+
category.data.resourceCollection === resourceCollection &&
|
|
557
|
+
category.data.resourceId === resourceId &&
|
|
558
|
+
category.data.resourceVersion === resourceVersion
|
|
559
|
+
);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
export const getResourceDocsForResource = async (
|
|
563
|
+
resourceCollection: ResourceCollection,
|
|
564
|
+
resourceId: string,
|
|
565
|
+
resourceVersion: string
|
|
566
|
+
): Promise<ResourceDocEntry[]> => {
|
|
567
|
+
const docs = await getResourceDocs();
|
|
568
|
+
return docs.filter(
|
|
569
|
+
(doc) =>
|
|
570
|
+
doc.data.resourceCollection === resourceCollection &&
|
|
571
|
+
doc.data.resourceId === resourceId &&
|
|
572
|
+
doc.data.resourceVersion === resourceVersion
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export const getGroupedResourceDocsByType = (
|
|
577
|
+
docs: ResourceDocEntry[],
|
|
578
|
+
{ latestOnly = true, categories = [] }: { latestOnly?: boolean; categories?: ResourceDocCategoryEntry[] } = {}
|
|
579
|
+
): ResourceDocGroup[] => {
|
|
580
|
+
const docsByType = new Map<string, ResourceDocEntry[]>();
|
|
581
|
+
const categoriesByType = new Map(categories.map((category) => [normalizeTypeName(category.data.type), category]));
|
|
582
|
+
|
|
583
|
+
const findCategoryForType = (type: string): ResourceDocCategoryEntry | undefined => {
|
|
584
|
+
const normalizedType = normalizeTypeName(type);
|
|
585
|
+
const directMatch = categoriesByType.get(normalizedType);
|
|
586
|
+
if (directMatch) {
|
|
587
|
+
return directMatch;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (normalizedType.endsWith('s')) {
|
|
591
|
+
return categoriesByType.get(normalizedType.slice(0, -1));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return categoriesByType.get(`${normalizedType}s`);
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
for (const doc of docs) {
|
|
598
|
+
if (latestOnly && doc.data.version !== doc.data.latestVersion) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const list = docsByType.get(doc.data.type) || [];
|
|
602
|
+
list.push(doc);
|
|
603
|
+
docsByType.set(doc.data.type, list);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return [...docsByType.entries()]
|
|
607
|
+
.map(([type, typeDocs]) => {
|
|
608
|
+
const category = findCategoryForType(type);
|
|
609
|
+
return {
|
|
610
|
+
type,
|
|
611
|
+
label: category?.data.label,
|
|
612
|
+
position: category?.data.position,
|
|
613
|
+
docs: [...typeDocs].sort((a, b) => {
|
|
614
|
+
const orderA = typeof a.data.order === 'number' ? a.data.order : Number.POSITIVE_INFINITY;
|
|
615
|
+
const orderB = typeof b.data.order === 'number' ? b.data.order : Number.POSITIVE_INFINITY;
|
|
616
|
+
|
|
617
|
+
if (orderA !== orderB) {
|
|
618
|
+
return orderA - orderB;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const titleA = (a.data.title || a.data.id).toLowerCase();
|
|
622
|
+
const titleB = (b.data.title || b.data.id).toLowerCase();
|
|
623
|
+
const titleCompare = titleA.localeCompare(titleB);
|
|
624
|
+
if (titleCompare !== 0) {
|
|
625
|
+
return titleCompare;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const semverA = coerce(a.data.version);
|
|
629
|
+
const semverB = coerce(b.data.version);
|
|
630
|
+
if (semverA && semverB) {
|
|
631
|
+
return rcompare(semverA, semverB);
|
|
632
|
+
}
|
|
633
|
+
return b.data.version.localeCompare(a.data.version);
|
|
634
|
+
}),
|
|
635
|
+
};
|
|
636
|
+
})
|
|
637
|
+
.sort((a, b) => {
|
|
638
|
+
const positionA = typeof a.position === 'number' ? a.position : Number.POSITIVE_INFINITY;
|
|
639
|
+
const positionB = typeof b.position === 'number' ? b.position : Number.POSITIVE_INFINITY;
|
|
640
|
+
|
|
641
|
+
if (positionA !== positionB) {
|
|
642
|
+
return positionA - positionB;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return a.type.localeCompare(b.type);
|
|
646
|
+
});
|
|
647
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the EventCatalog Commercial License.
|
|
3
|
+
* See /packages/core/eventcatalog/src/enterprise/LICENSE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'astro/zod';
|
|
7
|
+
import { badge } from '../../content.config-shared-collections';
|
|
8
|
+
|
|
9
|
+
export const resourceDocsSchema = z.object({
|
|
10
|
+
id: z.string().optional(),
|
|
11
|
+
type: z.string().optional(),
|
|
12
|
+
version: z.string().optional(),
|
|
13
|
+
order: z.number().optional(),
|
|
14
|
+
badges: z.array(badge).optional(),
|
|
15
|
+
title: z.string().optional(),
|
|
16
|
+
summary: z.string().optional(),
|
|
17
|
+
slug: z.string().optional(),
|
|
18
|
+
hidden: z.boolean().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const resourceDocCategoriesSchema = z.object({
|
|
22
|
+
label: z.string().optional(),
|
|
23
|
+
position: z.number().optional(),
|
|
24
|
+
});
|