@eventcatalog/core 3.25.6 → 3.26.1

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.
Files changed (35) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-P23BMUBV.js → chunk-6BTN7CY7.js} +1 -1
  6. package/dist/{chunk-ZEOK723Y.js → chunk-EL6ZQNAX.js} +1 -1
  7. package/dist/{chunk-53HXLUNO.js → chunk-LTWPA4SA.js} +1 -1
  8. package/dist/{chunk-R7P4GTFQ.js → chunk-N3QSCVYA.js} +1 -1
  9. package/dist/{chunk-2ILJMBQM.js → chunk-Y736FREK.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +562 -19
  13. package/dist/eventcatalog.js +576 -31
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/astro.config.mjs +2 -1
  19. package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
  20. package/eventcatalog/public/icons/graphql.svg +3 -1
  21. package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +225 -0
  22. package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +521 -0
  23. package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +501 -0
  24. package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +236 -0
  25. package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +241 -0
  26. package/eventcatalog/src/enterprise/fields/field-extractor.ts +183 -0
  27. package/eventcatalog/src/enterprise/fields/field-indexer.ts +131 -0
  28. package/eventcatalog/src/enterprise/fields/fields-db.test.ts +186 -0
  29. package/eventcatalog/src/enterprise/fields/fields-db.ts +453 -0
  30. package/eventcatalog/src/enterprise/fields/pages/api/fields.ts +43 -0
  31. package/eventcatalog/src/enterprise/fields/pages/fields.astro +19 -0
  32. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +23 -3
  33. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +14 -16
  34. package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +192 -0
  35. package/package.json +6 -4
@@ -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
+ });