@eventcatalog/core 3.43.1 → 3.44.0
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 +50 -2
- package/dist/analytics/log-build.js +4 -4
- package/dist/catalog-to-astro-content-directory.cjs +31 -1
- package/dist/catalog-to-astro-content-directory.js +2 -2
- package/dist/{chunk-O6KT4DPL.js → chunk-4SMA4HQ3.js} +1 -1
- package/dist/{chunk-5T63CXKU.js → chunk-6QENHZZP.js} +32 -2
- package/dist/{chunk-2GQO7I7E.js → chunk-A7WJATRV.js} +2 -2
- package/dist/{chunk-ULZYHF3V.js → chunk-B7HCX5HM.js} +1 -1
- package/dist/{chunk-C6S5P57F.js → chunk-H7IGB2WO.js} +1 -1
- package/dist/{chunk-Z5QHV4ZY.js → chunk-L2JZBH6K.js} +1 -1
- package/dist/{chunk-WAJIJEI3.js → chunk-LHR4G2UO.js} +1 -1
- package/dist/{chunk-OOX6HAE4.js → chunk-SWSSBJ6H.js} +20 -2
- package/dist/{chunk-KV5FCOV4.js → chunk-W7UIDHNR.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog-config-file-utils.cjs +31 -1
- package/dist/eventcatalog-config-file-utils.js +1 -1
- package/dist/eventcatalog.cjs +50 -2
- package/dist/eventcatalog.config.d.cts +25 -0
- package/dist/eventcatalog.config.d.ts +25 -0
- package/dist/eventcatalog.js +9 -9
- package/dist/features.cjs +31 -1
- package/dist/features.js +2 -2
- package/dist/generate.cjs +32 -2
- package/dist/generate.js +4 -4
- package/dist/resolve-catalog-dependencies.cjs +31 -1
- package/dist/resolve-catalog-dependencies.js +2 -2
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +9 -1
- package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +2 -2
- package/eventcatalog/src/components/Tables/Table.tsx +4 -0
- package/eventcatalog/src/components/Tables/columns/DirectorySourceColumn.tsx +49 -0
- package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +11 -0
- package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +11 -0
- package/eventcatalog/src/content.config.ts +29 -8
- package/eventcatalog/src/enterprise/directory/user-team-directory.spec.ts +527 -0
- package/eventcatalog/src/enterprise/directory/user-team-directory.ts +191 -0
- package/eventcatalog/src/pages/directory/[type]/index.astro +2 -0
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +29 -5
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +29 -0
- package/eventcatalog/src/stores/eventcatalog-store.spec.ts +60 -0
- package/eventcatalog/src/stores/eventcatalog-store.ts +103 -0
- package/package.json +3 -3
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const localLoad = vi.fn();
|
|
7
|
+
const isEventCatalogScaleEnabled = vi.fn();
|
|
8
|
+
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
9
|
+
|
|
10
|
+
vi.mock('astro/loaders', () => ({
|
|
11
|
+
glob: vi.fn(() => ({
|
|
12
|
+
name: 'mock-glob-loader',
|
|
13
|
+
load: localLoad,
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../feature', () => ({
|
|
18
|
+
isEventCatalogScaleEnabled: () => isEventCatalogScaleEnabled(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { userTeamDirectoryLoader } from './user-team-directory';
|
|
22
|
+
|
|
23
|
+
const createContentStore = () => {
|
|
24
|
+
const entries = new Map<string, unknown>();
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
has: (id: string) => entries.has(id),
|
|
28
|
+
set: (entry: { id: string; [key: string]: unknown }) => entries.set(entry.id, entry),
|
|
29
|
+
get: (id: string) => entries.get(id),
|
|
30
|
+
clear: () => entries.clear(),
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const createContext = () => ({
|
|
35
|
+
store: createContentStore(),
|
|
36
|
+
meta: new Map<string, string>(),
|
|
37
|
+
parseData: vi.fn(async ({ data }) => data),
|
|
38
|
+
renderMarkdown: vi.fn(async (markdown) => ({
|
|
39
|
+
html: `<p>${markdown}</p>`,
|
|
40
|
+
metadata: {
|
|
41
|
+
headings: [],
|
|
42
|
+
localImagePaths: [],
|
|
43
|
+
remoteImagePaths: [],
|
|
44
|
+
frontmatter: {},
|
|
45
|
+
imagePaths: [],
|
|
46
|
+
},
|
|
47
|
+
})),
|
|
48
|
+
generateDigest: vi.fn((data) => JSON.stringify(data)),
|
|
49
|
+
logger: {
|
|
50
|
+
info: vi.fn(),
|
|
51
|
+
warn: vi.fn(),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('userTeamDirectoryLoader', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
localLoad.mockReset();
|
|
58
|
+
isEventCatalogScaleEnabled.mockReset();
|
|
59
|
+
isEventCatalogScaleEnabled.mockReturnValue(false);
|
|
60
|
+
consoleLog.mockClear();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('loads local users without requiring EventCatalog Scale when no directory sources are configured', async () => {
|
|
64
|
+
const context = createContext();
|
|
65
|
+
const loader = userTeamDirectoryLoader({
|
|
66
|
+
collection: 'users',
|
|
67
|
+
local: {
|
|
68
|
+
pattern: 'users/*.(md|mdx)',
|
|
69
|
+
base: '/catalog',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await loader.load(context as never);
|
|
74
|
+
|
|
75
|
+
expect(localLoad).toHaveBeenCalledWith(context);
|
|
76
|
+
expect(isEventCatalogScaleEnabled).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('requires EventCatalog Scale when directory sources are configured', async () => {
|
|
80
|
+
const context = createContext();
|
|
81
|
+
const loader = userTeamDirectoryLoader({
|
|
82
|
+
collection: 'users',
|
|
83
|
+
local: {
|
|
84
|
+
pattern: 'users/*.(md|mdx)',
|
|
85
|
+
base: '/catalog',
|
|
86
|
+
},
|
|
87
|
+
storePath: false,
|
|
88
|
+
sources: [
|
|
89
|
+
{
|
|
90
|
+
type: 'directory',
|
|
91
|
+
name: 'test-source',
|
|
92
|
+
loadUsers: async () => [],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await expect(loader.load(context as never)).rejects.toThrow('Directory sources require EventCatalog Scale.');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('loads users from configured directory sources', async () => {
|
|
101
|
+
isEventCatalogScaleEnabled.mockReturnValue(true);
|
|
102
|
+
const context = createContext();
|
|
103
|
+
const loader = userTeamDirectoryLoader({
|
|
104
|
+
collection: 'users',
|
|
105
|
+
local: {
|
|
106
|
+
pattern: 'users/*.(md|mdx)',
|
|
107
|
+
base: '/catalog',
|
|
108
|
+
},
|
|
109
|
+
storePath: false,
|
|
110
|
+
sources: [
|
|
111
|
+
{
|
|
112
|
+
type: 'directory',
|
|
113
|
+
name: 'test-source',
|
|
114
|
+
loadUsers: async () => [
|
|
115
|
+
{
|
|
116
|
+
id: 'jane',
|
|
117
|
+
name: 'Jane Doe',
|
|
118
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
119
|
+
markdown: '# Jane',
|
|
120
|
+
source: {
|
|
121
|
+
provider: 'github',
|
|
122
|
+
id: 'github-user-123',
|
|
123
|
+
url: 'https://github.com/jane',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await loader.load(context as never);
|
|
132
|
+
|
|
133
|
+
expect(context.parseData).toHaveBeenCalledWith({
|
|
134
|
+
id: 'jane',
|
|
135
|
+
data: {
|
|
136
|
+
id: 'jane',
|
|
137
|
+
name: 'Jane Doe',
|
|
138
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
139
|
+
source: {
|
|
140
|
+
provider: 'github',
|
|
141
|
+
id: 'github-user-123',
|
|
142
|
+
url: 'https://github.com/jane',
|
|
143
|
+
},
|
|
144
|
+
readOnly: true,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
expect(context.store.get('jane')).toEqual({
|
|
148
|
+
id: 'jane',
|
|
149
|
+
data: {
|
|
150
|
+
id: 'jane',
|
|
151
|
+
name: 'Jane Doe',
|
|
152
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
153
|
+
source: {
|
|
154
|
+
provider: 'github',
|
|
155
|
+
id: 'github-user-123',
|
|
156
|
+
url: 'https://github.com/jane',
|
|
157
|
+
},
|
|
158
|
+
readOnly: true,
|
|
159
|
+
},
|
|
160
|
+
body: '# Jane',
|
|
161
|
+
rendered: {
|
|
162
|
+
html: '<p># Jane</p>',
|
|
163
|
+
metadata: {
|
|
164
|
+
headings: [],
|
|
165
|
+
localImagePaths: [],
|
|
166
|
+
remoteImagePaths: [],
|
|
167
|
+
frontmatter: {},
|
|
168
|
+
imagePaths: [],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
digest: JSON.stringify({
|
|
172
|
+
id: 'jane',
|
|
173
|
+
name: 'Jane Doe',
|
|
174
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
175
|
+
source: {
|
|
176
|
+
provider: 'github',
|
|
177
|
+
id: 'github-user-123',
|
|
178
|
+
url: 'https://github.com/jane',
|
|
179
|
+
},
|
|
180
|
+
readOnly: true,
|
|
181
|
+
body: '# Jane',
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
expect(consoleLog).toHaveBeenCalledWith(expect.stringContaining('Loading users from directory source "test-source"'));
|
|
185
|
+
expect(consoleLog).toHaveBeenCalledWith(expect.stringContaining('Synced 1 users from directory source "test-source"'));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('keeps local entries when the conflict strategy is local-wins', async () => {
|
|
189
|
+
isEventCatalogScaleEnabled.mockReturnValue(true);
|
|
190
|
+
const context = createContext();
|
|
191
|
+
context.store.set({
|
|
192
|
+
id: 'jane',
|
|
193
|
+
data: {
|
|
194
|
+
id: 'jane',
|
|
195
|
+
name: 'Local Jane',
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const loader = userTeamDirectoryLoader({
|
|
199
|
+
collection: 'users',
|
|
200
|
+
local: {
|
|
201
|
+
pattern: 'users/*.(md|mdx)',
|
|
202
|
+
base: '/catalog',
|
|
203
|
+
},
|
|
204
|
+
storePath: false,
|
|
205
|
+
sources: [
|
|
206
|
+
{
|
|
207
|
+
type: 'directory',
|
|
208
|
+
name: 'test-source',
|
|
209
|
+
loadUsers: async () => [
|
|
210
|
+
{
|
|
211
|
+
id: 'jane',
|
|
212
|
+
name: 'Source Jane',
|
|
213
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await loader.load(context as never);
|
|
221
|
+
|
|
222
|
+
expect(context.store.get('jane')).toEqual({
|
|
223
|
+
id: 'jane',
|
|
224
|
+
data: {
|
|
225
|
+
id: 'jane',
|
|
226
|
+
name: 'Local Jane',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
expect(consoleLog).toHaveBeenCalledWith(
|
|
230
|
+
expect.stringContaining('Synced 0 users from directory source "test-source" (1 skipped due to local conflicts)')
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('throws when the conflict strategy is error and a source returns a local id', async () => {
|
|
235
|
+
isEventCatalogScaleEnabled.mockReturnValue(true);
|
|
236
|
+
const context = createContext();
|
|
237
|
+
context.store.set({
|
|
238
|
+
id: 'jane',
|
|
239
|
+
data: {
|
|
240
|
+
id: 'jane',
|
|
241
|
+
name: 'Local Jane',
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const loader = userTeamDirectoryLoader({
|
|
245
|
+
collection: 'users',
|
|
246
|
+
local: {
|
|
247
|
+
pattern: 'users/*.(md|mdx)',
|
|
248
|
+
base: '/catalog',
|
|
249
|
+
},
|
|
250
|
+
conflictStrategy: 'error',
|
|
251
|
+
storePath: false,
|
|
252
|
+
sources: [
|
|
253
|
+
{
|
|
254
|
+
type: 'directory',
|
|
255
|
+
name: 'test-source',
|
|
256
|
+
loadUsers: async () => [
|
|
257
|
+
{
|
|
258
|
+
id: 'jane',
|
|
259
|
+
name: 'Source Jane',
|
|
260
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await expect(loader.load(context as never)).rejects.toThrow(
|
|
268
|
+
'Directory source "test-source" returned duplicate users id "jane".'
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('refreshes source entries on subsequent loads', async () => {
|
|
273
|
+
isEventCatalogScaleEnabled.mockReturnValue(true);
|
|
274
|
+
const context = createContext();
|
|
275
|
+
const loadUsers = vi
|
|
276
|
+
.fn()
|
|
277
|
+
.mockResolvedValueOnce([
|
|
278
|
+
{
|
|
279
|
+
id: 'jane',
|
|
280
|
+
name: 'Jane Doe',
|
|
281
|
+
avatarUrl: 'https://example.com/jane.png',
|
|
282
|
+
},
|
|
283
|
+
])
|
|
284
|
+
.mockResolvedValueOnce([
|
|
285
|
+
{
|
|
286
|
+
id: 'jane',
|
|
287
|
+
name: 'Jane Updated',
|
|
288
|
+
avatarUrl: 'https://example.com/jane-updated.png',
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
const loader = userTeamDirectoryLoader({
|
|
292
|
+
collection: 'users',
|
|
293
|
+
local: {
|
|
294
|
+
pattern: 'users/*.(md|mdx)',
|
|
295
|
+
base: '/catalog',
|
|
296
|
+
},
|
|
297
|
+
storePath: false,
|
|
298
|
+
sources: [
|
|
299
|
+
{
|
|
300
|
+
type: 'directory',
|
|
301
|
+
name: 'test-source',
|
|
302
|
+
loadUsers,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await loader.load(context as never);
|
|
308
|
+
context.store.clear();
|
|
309
|
+
await loader.load(context as never);
|
|
310
|
+
|
|
311
|
+
expect(loadUsers).toHaveBeenCalledTimes(2);
|
|
312
|
+
expect(context.store.get('jane')).toMatchObject({
|
|
313
|
+
data: {
|
|
314
|
+
name: 'Jane Updated',
|
|
315
|
+
avatarUrl: 'https://example.com/jane-updated.png',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('writes synced source entries to the EventCatalog directory store', async () => {
|
|
321
|
+
isEventCatalogScaleEnabled.mockReturnValue(true);
|
|
322
|
+
const context = createContext();
|
|
323
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'eventcatalog-directory-store-'));
|
|
324
|
+
const storePath = path.join(tempDir, '.eventcatalog', 'store', 'directory.json');
|
|
325
|
+
const loader = userTeamDirectoryLoader({
|
|
326
|
+
collection: 'teams',
|
|
327
|
+
local: {
|
|
328
|
+
pattern: 'teams/*.(md|mdx)',
|
|
329
|
+
base: tempDir,
|
|
330
|
+
},
|
|
331
|
+
storePath,
|
|
332
|
+
sources: [
|
|
333
|
+
{
|
|
334
|
+
type: 'directory',
|
|
335
|
+
name: 'github:event-catalog',
|
|
336
|
+
loadTeams: async () => [
|
|
337
|
+
{
|
|
338
|
+
id: 'core-maintainers',
|
|
339
|
+
name: 'Core Maintainers',
|
|
340
|
+
markdown: '# Core Maintainers',
|
|
341
|
+
source: {
|
|
342
|
+
provider: 'github',
|
|
343
|
+
id: 'github-team-123',
|
|
344
|
+
url: 'https://github.com/orgs/event-catalog/teams/core-maintainers',
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
await loader.load(context as never);
|
|
354
|
+
|
|
355
|
+
const store = JSON.parse(await readFile(storePath, 'utf8'));
|
|
356
|
+
expect(store).toMatchObject({
|
|
357
|
+
version: '1',
|
|
358
|
+
resources: {
|
|
359
|
+
users: [],
|
|
360
|
+
teams: [
|
|
361
|
+
{
|
|
362
|
+
id: 'core-maintainers',
|
|
363
|
+
name: 'Core Maintainers',
|
|
364
|
+
markdown: '# Core Maintainers',
|
|
365
|
+
readOnly: true,
|
|
366
|
+
source: {
|
|
367
|
+
provider: 'github',
|
|
368
|
+
id: 'github-team-123',
|
|
369
|
+
url: 'https://github.com/orgs/event-catalog/teams/core-maintainers',
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
expect(store.generatedAt).toEqual(expect.any(String));
|
|
376
|
+
} finally {
|
|
377
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('deduplicates synced store entries when source-wins replaces an earlier directory source entry', async () => {
|
|
382
|
+
isEventCatalogScaleEnabled.mockReturnValue(true);
|
|
383
|
+
const context = createContext();
|
|
384
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'eventcatalog-directory-store-'));
|
|
385
|
+
const storePath = path.join(tempDir, '.eventcatalog', 'store', 'directory.json');
|
|
386
|
+
const loader = userTeamDirectoryLoader({
|
|
387
|
+
collection: 'users',
|
|
388
|
+
local: {
|
|
389
|
+
pattern: 'users/*.(md|mdx)',
|
|
390
|
+
base: tempDir,
|
|
391
|
+
},
|
|
392
|
+
conflictStrategy: 'source-wins',
|
|
393
|
+
storePath,
|
|
394
|
+
sources: [
|
|
395
|
+
{
|
|
396
|
+
type: 'directory',
|
|
397
|
+
name: 'github:first',
|
|
398
|
+
loadUsers: async () => [
|
|
399
|
+
{
|
|
400
|
+
id: 'jane',
|
|
401
|
+
name: 'Jane From First Source',
|
|
402
|
+
markdown: 'First source Jane',
|
|
403
|
+
source: {
|
|
404
|
+
provider: 'github',
|
|
405
|
+
id: 'first-source-jane',
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: 'directory',
|
|
412
|
+
name: 'github:second',
|
|
413
|
+
loadUsers: async () => [
|
|
414
|
+
{
|
|
415
|
+
id: 'jane',
|
|
416
|
+
name: 'Jane From Second Source',
|
|
417
|
+
markdown: 'Second source Jane',
|
|
418
|
+
source: {
|
|
419
|
+
provider: 'github',
|
|
420
|
+
id: 'second-source-jane',
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
await loader.load(context as never);
|
|
430
|
+
|
|
431
|
+
const store = JSON.parse(await readFile(storePath, 'utf8'));
|
|
432
|
+
expect(store.resources.users).toEqual([
|
|
433
|
+
{
|
|
434
|
+
id: 'jane',
|
|
435
|
+
name: 'Jane From Second Source',
|
|
436
|
+
markdown: 'Second source Jane',
|
|
437
|
+
readOnly: true,
|
|
438
|
+
source: {
|
|
439
|
+
provider: 'github',
|
|
440
|
+
id: 'second-source-jane',
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
]);
|
|
444
|
+
expect(context.store.get('jane')).toMatchObject({
|
|
445
|
+
data: {
|
|
446
|
+
id: 'jane',
|
|
447
|
+
name: 'Jane From Second Source',
|
|
448
|
+
source: {
|
|
449
|
+
provider: 'github',
|
|
450
|
+
id: 'second-source-jane',
|
|
451
|
+
},
|
|
452
|
+
readOnly: true,
|
|
453
|
+
},
|
|
454
|
+
body: 'Second source Jane',
|
|
455
|
+
});
|
|
456
|
+
} finally {
|
|
457
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('clears an existing directory store collection when sources are removed', async () => {
|
|
462
|
+
const context = createContext();
|
|
463
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'eventcatalog-directory-store-'));
|
|
464
|
+
const storePath = path.join(tempDir, '.eventcatalog', 'store', 'directory.json');
|
|
465
|
+
const loader = userTeamDirectoryLoader({
|
|
466
|
+
collection: 'users',
|
|
467
|
+
local: {
|
|
468
|
+
pattern: 'users/*.(md|mdx)',
|
|
469
|
+
base: tempDir,
|
|
470
|
+
},
|
|
471
|
+
storePath,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
await mkdir(path.dirname(storePath), { recursive: true });
|
|
476
|
+
await writeFile(
|
|
477
|
+
storePath,
|
|
478
|
+
JSON.stringify({
|
|
479
|
+
version: '1',
|
|
480
|
+
generatedAt: '2026-05-27T00:00:00.000Z',
|
|
481
|
+
resources: {
|
|
482
|
+
users: [
|
|
483
|
+
{
|
|
484
|
+
id: 'stale-user',
|
|
485
|
+
name: 'Stale User',
|
|
486
|
+
markdown: 'Stale directory user',
|
|
487
|
+
readOnly: true,
|
|
488
|
+
source: {
|
|
489
|
+
provider: 'github',
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
teams: [
|
|
494
|
+
{
|
|
495
|
+
id: 'existing-team',
|
|
496
|
+
name: 'Existing Team',
|
|
497
|
+
markdown: 'Existing directory team',
|
|
498
|
+
readOnly: true,
|
|
499
|
+
source: {
|
|
500
|
+
provider: 'github',
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
await loader.load(context as never);
|
|
509
|
+
|
|
510
|
+
const store = JSON.parse(await readFile(storePath, 'utf8'));
|
|
511
|
+
expect(store.resources.users).toEqual([]);
|
|
512
|
+
expect(store.resources.teams).toEqual([
|
|
513
|
+
{
|
|
514
|
+
id: 'existing-team',
|
|
515
|
+
name: 'Existing Team',
|
|
516
|
+
markdown: 'Existing directory team',
|
|
517
|
+
readOnly: true,
|
|
518
|
+
source: {
|
|
519
|
+
provider: 'github',
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
]);
|
|
523
|
+
} finally {
|
|
524
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { glob, type Loader } from 'astro/loaders';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { isEventCatalogScaleEnabled } from '../feature';
|
|
4
|
+
import { EventCatalogStore } from '../../stores/eventcatalog-store';
|
|
5
|
+
|
|
6
|
+
const colors = pc.createColors(true);
|
|
7
|
+
|
|
8
|
+
type UserTeamCollection = 'users' | 'teams';
|
|
9
|
+
type GlobOptions = Parameters<typeof glob>[0];
|
|
10
|
+
|
|
11
|
+
type DirectoryEntry = {
|
|
12
|
+
id: string;
|
|
13
|
+
markdown?: string;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
source?: {
|
|
16
|
+
provider?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type DirectorySource = {
|
|
24
|
+
type: 'directory';
|
|
25
|
+
name: string;
|
|
26
|
+
loadUsers?: () => Promise<DirectoryEntry[]>;
|
|
27
|
+
loadTeams?: () => Promise<DirectoryEntry[]>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type DirectoryConflictStrategy = 'local-wins' | 'source-wins' | 'error';
|
|
31
|
+
|
|
32
|
+
type UserTeamDirectoryLoaderOptions = {
|
|
33
|
+
collection: UserTeamCollection;
|
|
34
|
+
local: GlobOptions;
|
|
35
|
+
sources?: DirectorySource[];
|
|
36
|
+
conflictStrategy?: DirectoryConflictStrategy;
|
|
37
|
+
storePath?: string | false;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type DirectoryStoreResource = DirectoryEntry & {
|
|
41
|
+
id: string;
|
|
42
|
+
markdown: string;
|
|
43
|
+
readOnly: true;
|
|
44
|
+
source: NonNullable<DirectoryEntry['source']> & {
|
|
45
|
+
provider: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type DirectoryStoreResources = {
|
|
50
|
+
users: DirectoryStoreResource[];
|
|
51
|
+
teams: DirectoryStoreResource[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const userTeamDirectoryLoader = ({
|
|
55
|
+
collection,
|
|
56
|
+
local,
|
|
57
|
+
sources = [],
|
|
58
|
+
conflictStrategy = 'local-wins',
|
|
59
|
+
storePath,
|
|
60
|
+
}: UserTeamDirectoryLoaderOptions): Loader => {
|
|
61
|
+
const localLoader = glob(local);
|
|
62
|
+
const directoryStore = createDirectoryStore({ base: local.base, storePath });
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name: `eventcatalog-${collection}-directory-loader`,
|
|
66
|
+
load: async (context) => {
|
|
67
|
+
await localLoader.load(context);
|
|
68
|
+
|
|
69
|
+
if (sources.length === 0) {
|
|
70
|
+
await directoryStore?.clearCollectionIfStoreExists(collection);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!isEventCatalogScaleEnabled()) {
|
|
75
|
+
throw new Error('Directory sources require EventCatalog Scale.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const loadEntries = collection === 'users' ? 'loadUsers' : 'loadTeams';
|
|
79
|
+
const syncedDirectoryStoreResources = new Map<string, DirectoryStoreResource>();
|
|
80
|
+
|
|
81
|
+
for (const source of sources) {
|
|
82
|
+
logDirectoryInfo(`Loading ${collection} from directory source "${source.name}"`);
|
|
83
|
+
const entries = await loadSourceEntries({ source, loadEntries });
|
|
84
|
+
let syncedEntries = 0;
|
|
85
|
+
let skippedEntries = 0;
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (context.store.has(entry.id)) {
|
|
89
|
+
if (conflictStrategy === 'local-wins') {
|
|
90
|
+
skippedEntries += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (conflictStrategy === 'error') {
|
|
94
|
+
throw new Error(`Directory source "${source.name}" returned duplicate ${collection} id "${entry.id}".`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { markdown, ...entryData } = entry;
|
|
99
|
+
const data = {
|
|
100
|
+
...withDirectoryEntrySource(entryData, source),
|
|
101
|
+
id: entry.id,
|
|
102
|
+
readOnly: true,
|
|
103
|
+
};
|
|
104
|
+
const body = markdown ?? '';
|
|
105
|
+
const rendered = body ? await context.renderMarkdown(body) : undefined;
|
|
106
|
+
const parsedData = await context.parseData({
|
|
107
|
+
id: entry.id,
|
|
108
|
+
data,
|
|
109
|
+
});
|
|
110
|
+
syncedDirectoryStoreResources.set(entry.id, {
|
|
111
|
+
...data,
|
|
112
|
+
markdown: body,
|
|
113
|
+
readOnly: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
context.store.set({
|
|
117
|
+
id: entry.id,
|
|
118
|
+
data: parsedData,
|
|
119
|
+
body,
|
|
120
|
+
digest: context.generateDigest({ ...data, body }),
|
|
121
|
+
rendered,
|
|
122
|
+
});
|
|
123
|
+
syncedEntries += 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logDirectoryInfo(
|
|
127
|
+
`Synced ${syncedEntries} ${collection} from directory source "${source.name}"${
|
|
128
|
+
skippedEntries > 0 ? ` (${skippedEntries} skipped due to local conflicts)` : ''
|
|
129
|
+
}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await directoryStore?.writeCollection(collection, Array.from(syncedDirectoryStoreResources.values()));
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const loadSourceEntries = async ({
|
|
139
|
+
source,
|
|
140
|
+
loadEntries,
|
|
141
|
+
}: {
|
|
142
|
+
source: DirectorySource;
|
|
143
|
+
loadEntries: 'loadUsers' | 'loadTeams';
|
|
144
|
+
}) => {
|
|
145
|
+
return (await source[loadEntries]?.()) ?? [];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getSourceProvider = (source: DirectorySource) => source.name.split(':')[0] || source.name;
|
|
149
|
+
|
|
150
|
+
const withDirectoryEntrySource = (entryData: Omit<DirectoryEntry, 'markdown'>, source: DirectorySource) => {
|
|
151
|
+
const entrySource = entryData.source as DirectoryEntry['source'] | undefined;
|
|
152
|
+
const provider = entrySource?.provider ?? getSourceProvider(source);
|
|
153
|
+
const sourceData = {
|
|
154
|
+
...entrySource,
|
|
155
|
+
provider,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...entryData,
|
|
160
|
+
source: sourceData,
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const getDirectoryStorePath = (base: GlobOptions['base']) => {
|
|
165
|
+
if (!base || typeof base !== 'string') return undefined;
|
|
166
|
+
return EventCatalogStore.getStorePath(base, 'directory');
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const createDirectoryStore = ({ base, storePath }: { base: GlobOptions['base']; storePath?: string | false }) => {
|
|
170
|
+
if (storePath === false) return undefined;
|
|
171
|
+
|
|
172
|
+
const resolvedStorePath = storePath ?? getDirectoryStorePath(base);
|
|
173
|
+
if (!resolvedStorePath) return undefined;
|
|
174
|
+
|
|
175
|
+
return new EventCatalogStore<DirectoryStoreResources>({
|
|
176
|
+
storePath: resolvedStorePath,
|
|
177
|
+
resources: {
|
|
178
|
+
users: [],
|
|
179
|
+
teams: [],
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const getTimestamp = () => {
|
|
185
|
+
const now = new Date();
|
|
186
|
+
return now.toLocaleTimeString('en-US', { hour12: false });
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const logDirectoryInfo = (message: string) => {
|
|
190
|
+
console.log(`${colors.dim(getTimestamp())} ${colors.blue('[directory]')} ${message}`);
|
|
191
|
+
};
|