@eventcatalog/core 3.25.5 → 3.26.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 +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/catalog-to-astro-content-directory.cjs +1 -1
- package/dist/{chunk-NIGGP5OH.js → chunk-7CRFNX47.js} +1 -1
- package/dist/{chunk-G3KLCHZ7.js → chunk-ASC3AR2X.js} +1 -1
- package/dist/{chunk-KMBHKZX5.js → chunk-FQNBDDUF.js} +1 -1
- package/dist/{chunk-TQQ3EOI3.js → chunk-GCNIIIFG.js} +1 -1
- package/dist/{chunk-6QMOE3KE.js → chunk-XUN32ZVJ.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +563 -20
- package/dist/eventcatalog.js +572 -27
- 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 +2 -1
- package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
- package/eventcatalog/public/icons/graphql.svg +3 -1
- package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +225 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +521 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +501 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +236 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +241 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.ts +183 -0
- package/eventcatalog/src/enterprise/fields/field-indexer.ts +131 -0
- package/eventcatalog/src/enterprise/fields/fields-db.test.ts +186 -0
- package/eventcatalog/src/enterprise/fields/fields-db.ts +453 -0
- package/eventcatalog/src/enterprise/fields/pages/api/fields.ts +43 -0
- package/eventcatalog/src/enterprise/fields/pages/fields.astro +19 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +23 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +14 -16
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +192 -0
- package/package.json +11 -9
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { FieldsDatabase } from './fields-db';
|
|
4
|
+
import { extractSchemaFieldsDeep } from './field-extractor';
|
|
5
|
+
|
|
6
|
+
function detectFormat(fileName: string): string {
|
|
7
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
8
|
+
if (ext === '.proto') return 'proto';
|
|
9
|
+
if (ext === '.avro' || ext === '.avsc') return 'avro';
|
|
10
|
+
return 'json-schema';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Warning {
|
|
14
|
+
messageId: string;
|
|
15
|
+
version: string;
|
|
16
|
+
error: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the fields SQLite index using the EventCatalog SDK.
|
|
21
|
+
* This runs BEFORE Astro starts (no Astro dependencies).
|
|
22
|
+
*
|
|
23
|
+
* @param catalogDir - Path to the EventCatalog content directory
|
|
24
|
+
* @param outputDir - Path to write the .eventcatalog/fields.db (defaults to catalogDir)
|
|
25
|
+
*/
|
|
26
|
+
export async function buildFieldsIndex(catalogDir: string, outputDir?: string): Promise<{ dbPath: string; warnings: Warning[] }> {
|
|
27
|
+
// Dynamic import to avoid bundling the SDK into the Astro build
|
|
28
|
+
const sdkModule = await import('@eventcatalog/sdk');
|
|
29
|
+
const sdk = sdkModule.default(catalogDir);
|
|
30
|
+
|
|
31
|
+
const dbDir = path.join(outputDir || catalogDir, '.eventcatalog');
|
|
32
|
+
const dbPath = path.join(dbDir, 'fields.db');
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(dbDir)) {
|
|
35
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const db = new FieldsDatabase(dbPath, { recreate: true });
|
|
39
|
+
const warnings: Warning[] = [];
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Fetch all messages (latest version only) using the SDK
|
|
43
|
+
const [events, commands, queries] = await Promise.all([
|
|
44
|
+
sdk.getEvents({ latestOnly: true }),
|
|
45
|
+
sdk.getCommands({ latestOnly: true }),
|
|
46
|
+
sdk.getQueries({ latestOnly: true }),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const collections = [
|
|
50
|
+
{ entries: events, type: 'event' as const },
|
|
51
|
+
{ entries: commands, type: 'command' as const },
|
|
52
|
+
{ entries: queries, type: 'query' as const },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
for (const { entries, type } of collections) {
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const msgId = entry.id;
|
|
58
|
+
const msgVersion = entry.version;
|
|
59
|
+
|
|
60
|
+
// Get schema content via SDK
|
|
61
|
+
let schemaData: { schema: string; fileName: string } | undefined;
|
|
62
|
+
try {
|
|
63
|
+
schemaData = await sdk.getSchemaForMessage(msgId, msgVersion);
|
|
64
|
+
} catch {
|
|
65
|
+
// No schema for this message
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!schemaData) continue;
|
|
70
|
+
|
|
71
|
+
const { schema: content, fileName } = schemaData;
|
|
72
|
+
const format = detectFormat(fileName);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const fields = extractSchemaFieldsDeep(content, format);
|
|
76
|
+
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
db.insertField({
|
|
79
|
+
path: field.path,
|
|
80
|
+
type: field.type,
|
|
81
|
+
description: field.description,
|
|
82
|
+
required: field.required,
|
|
83
|
+
schemaFormat: format,
|
|
84
|
+
messageId: msgId,
|
|
85
|
+
messageVersion: msgVersion,
|
|
86
|
+
messageType: type,
|
|
87
|
+
messageName: entry.name || msgId,
|
|
88
|
+
messageSummary: entry.summary || '',
|
|
89
|
+
messageOwners: entry.owners || [],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get producers and consumers via SDK
|
|
94
|
+
const { producers, consumers } = await sdk.getProducersAndConsumersForMessage(msgId, msgVersion);
|
|
95
|
+
|
|
96
|
+
for (const producer of producers) {
|
|
97
|
+
db.insertProducer(
|
|
98
|
+
msgId,
|
|
99
|
+
msgVersion,
|
|
100
|
+
producer.id,
|
|
101
|
+
producer.version,
|
|
102
|
+
producer.name || producer.id,
|
|
103
|
+
producer.summary || '',
|
|
104
|
+
producer.owners || []
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
for (const consumer of consumers) {
|
|
108
|
+
db.insertConsumer(
|
|
109
|
+
msgId,
|
|
110
|
+
msgVersion,
|
|
111
|
+
consumer.id,
|
|
112
|
+
consumer.version,
|
|
113
|
+
consumer.name || consumer.id,
|
|
114
|
+
consumer.summary || '',
|
|
115
|
+
consumer.owners || []
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} catch (err: any) {
|
|
119
|
+
warnings.push({ messageId: msgId, version: msgVersion, error: err.message });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
db.rebuildFts();
|
|
125
|
+
db.close();
|
|
126
|
+
return { dbPath, warnings };
|
|
127
|
+
} catch (err) {
|
|
128
|
+
db.close();
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { FieldsDatabase } from './fields-db';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
|
|
7
|
+
describe('FieldsDatabase', () => {
|
|
8
|
+
let db: FieldsDatabase;
|
|
9
|
+
let tmpDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fields-db-'));
|
|
13
|
+
db = new FieldsDatabase(path.join(tmpDir, 'fields.db'), { recreate: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
db.close();
|
|
18
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('creates the schema tables on initialization', () => {
|
|
22
|
+
const tables = db.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
23
|
+
const tableNames = tables.map((t: any) => t.name);
|
|
24
|
+
expect(tableNames).toContain('fields');
|
|
25
|
+
expect(tableNames).toContain('message_producers');
|
|
26
|
+
expect(tableNames).toContain('message_consumers');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('inserts fields and retrieves them with pagination', () => {
|
|
30
|
+
db.insertField({
|
|
31
|
+
path: 'orderId',
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'The order ID',
|
|
34
|
+
required: true,
|
|
35
|
+
schemaFormat: 'json-schema',
|
|
36
|
+
messageId: 'OrderCreated',
|
|
37
|
+
messageVersion: '1.0.0',
|
|
38
|
+
messageType: 'event',
|
|
39
|
+
});
|
|
40
|
+
db.insertField({
|
|
41
|
+
path: 'amount',
|
|
42
|
+
type: 'number',
|
|
43
|
+
description: '',
|
|
44
|
+
required: false,
|
|
45
|
+
schemaFormat: 'json-schema',
|
|
46
|
+
messageId: 'OrderCreated',
|
|
47
|
+
messageVersion: '1.0.0',
|
|
48
|
+
messageType: 'event',
|
|
49
|
+
});
|
|
50
|
+
db.rebuildFts();
|
|
51
|
+
const result = db.queryFields({});
|
|
52
|
+
expect(result.fields).toHaveLength(2);
|
|
53
|
+
expect(result.total).toBe(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('filters fields by full-text search matching field path', () => {
|
|
57
|
+
db.insertField({
|
|
58
|
+
path: 'orderId',
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: '',
|
|
61
|
+
required: true,
|
|
62
|
+
schemaFormat: 'json-schema',
|
|
63
|
+
messageId: 'OrderCreated',
|
|
64
|
+
messageVersion: '1.0.0',
|
|
65
|
+
messageType: 'event',
|
|
66
|
+
});
|
|
67
|
+
db.insertField({
|
|
68
|
+
path: 'amount',
|
|
69
|
+
type: 'number',
|
|
70
|
+
description: '',
|
|
71
|
+
required: false,
|
|
72
|
+
schemaFormat: 'json-schema',
|
|
73
|
+
messageId: 'OrderCreated',
|
|
74
|
+
messageVersion: '1.0.0',
|
|
75
|
+
messageType: 'event',
|
|
76
|
+
});
|
|
77
|
+
db.rebuildFts();
|
|
78
|
+
const result = db.queryFields({ q: 'orderId' });
|
|
79
|
+
expect(result.fields).toHaveLength(1);
|
|
80
|
+
expect(result.fields[0].path).toBe('orderId');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('filters fields that appear in multiple messages when shared flag is set', () => {
|
|
84
|
+
db.insertField({
|
|
85
|
+
path: 'customerId',
|
|
86
|
+
type: 'string',
|
|
87
|
+
description: '',
|
|
88
|
+
required: true,
|
|
89
|
+
schemaFormat: 'json-schema',
|
|
90
|
+
messageId: 'OrderCreated',
|
|
91
|
+
messageVersion: '1.0.0',
|
|
92
|
+
messageType: 'event',
|
|
93
|
+
});
|
|
94
|
+
db.insertField({
|
|
95
|
+
path: 'customerId',
|
|
96
|
+
type: 'string',
|
|
97
|
+
description: '',
|
|
98
|
+
required: true,
|
|
99
|
+
schemaFormat: 'json-schema',
|
|
100
|
+
messageId: 'PaymentProcessed',
|
|
101
|
+
messageVersion: '1.0.0',
|
|
102
|
+
messageType: 'event',
|
|
103
|
+
});
|
|
104
|
+
db.insertField({
|
|
105
|
+
path: 'amount',
|
|
106
|
+
type: 'number',
|
|
107
|
+
description: '',
|
|
108
|
+
required: false,
|
|
109
|
+
schemaFormat: 'json-schema',
|
|
110
|
+
messageId: 'PaymentProcessed',
|
|
111
|
+
messageVersion: '1.0.0',
|
|
112
|
+
messageType: 'event',
|
|
113
|
+
});
|
|
114
|
+
db.rebuildFts();
|
|
115
|
+
const result = db.queryFields({ shared: true });
|
|
116
|
+
expect(result.fields.every((f) => f.path === 'customerId')).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('returns facet counts reflecting current filters', () => {
|
|
120
|
+
db.insertField({
|
|
121
|
+
path: 'orderId',
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: '',
|
|
124
|
+
required: true,
|
|
125
|
+
schemaFormat: 'json-schema',
|
|
126
|
+
messageId: 'OrderCreated',
|
|
127
|
+
messageVersion: '1.0.0',
|
|
128
|
+
messageType: 'event',
|
|
129
|
+
});
|
|
130
|
+
db.insertField({
|
|
131
|
+
path: 'status',
|
|
132
|
+
type: 'string',
|
|
133
|
+
description: '',
|
|
134
|
+
required: true,
|
|
135
|
+
schemaFormat: 'avro',
|
|
136
|
+
messageId: 'OrderUpdated',
|
|
137
|
+
messageVersion: '1.0.0',
|
|
138
|
+
messageType: 'event',
|
|
139
|
+
});
|
|
140
|
+
db.rebuildFts();
|
|
141
|
+
const result = db.queryFields({});
|
|
142
|
+
expect(result.facets.formats).toContainEqual({ value: 'json-schema', count: 1 });
|
|
143
|
+
expect(result.facets.formats).toContainEqual({ value: 'avro', count: 1 });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('paginates results using keyset cursor', () => {
|
|
147
|
+
for (let i = 0; i < 5; i++) {
|
|
148
|
+
db.insertField({
|
|
149
|
+
path: `field${i}`,
|
|
150
|
+
type: 'string',
|
|
151
|
+
description: '',
|
|
152
|
+
required: false,
|
|
153
|
+
schemaFormat: 'json-schema',
|
|
154
|
+
messageId: 'TestMsg',
|
|
155
|
+
messageVersion: '1.0.0',
|
|
156
|
+
messageType: 'event',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
db.rebuildFts();
|
|
160
|
+
const page1 = db.queryFields({ pageSize: 2 });
|
|
161
|
+
expect(page1.fields).toHaveLength(2);
|
|
162
|
+
expect(page1.cursor).toBeDefined();
|
|
163
|
+
const page2 = db.queryFields({ pageSize: 2, cursor: page1.cursor });
|
|
164
|
+
expect(page2.fields).toHaveLength(2);
|
|
165
|
+
expect(page2.fields[0].path).not.toBe(page1.fields[0].path);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('joins producer and consumer data for each field', () => {
|
|
169
|
+
db.insertField({
|
|
170
|
+
path: 'orderId',
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: '',
|
|
173
|
+
required: true,
|
|
174
|
+
schemaFormat: 'json-schema',
|
|
175
|
+
messageId: 'OrderCreated',
|
|
176
|
+
messageVersion: '1.0.0',
|
|
177
|
+
messageType: 'event',
|
|
178
|
+
});
|
|
179
|
+
db.insertProducer('OrderCreated', '1.0.0', 'OrderService', '1.0.0');
|
|
180
|
+
db.insertConsumer('OrderCreated', '1.0.0', 'InventoryService', '2.0.0');
|
|
181
|
+
db.rebuildFts();
|
|
182
|
+
const result = db.queryFields({});
|
|
183
|
+
expect(result.fields[0].producers).toEqual([{ id: 'OrderService', version: '1.0.0' }]);
|
|
184
|
+
expect(result.fields[0].consumers).toEqual([{ id: 'InventoryService', version: '2.0.0' }]);
|
|
185
|
+
});
|
|
186
|
+
});
|