@eventcatalog/core 3.25.6 → 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.
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-2ILJMBQM.js → chunk-7CRFNX47.js} +1 -1
  6. package/dist/{chunk-53HXLUNO.js → chunk-ASC3AR2X.js} +1 -1
  7. package/dist/{chunk-ZEOK723Y.js → chunk-FQNBDDUF.js} +1 -1
  8. package/dist/{chunk-P23BMUBV.js → chunk-GCNIIIFG.js} +1 -1
  9. package/dist/{chunk-R7P4GTFQ.js → chunk-XUN32ZVJ.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 +572 -27
  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 +4 -2
@@ -0,0 +1,453 @@
1
+ import Database from 'better-sqlite3';
2
+ import type BetterSqlite3 from 'better-sqlite3';
3
+ import fs from 'node:fs';
4
+
5
+ export interface FieldRow {
6
+ path: string;
7
+ type: string;
8
+ description: string;
9
+ required: boolean;
10
+ schemaFormat: string;
11
+ messageId: string;
12
+ messageVersion: string;
13
+ messageType: string;
14
+ }
15
+
16
+ export interface ServiceRef {
17
+ id: string;
18
+ version: string;
19
+ name?: string;
20
+ summary?: string;
21
+ owners?: string[];
22
+ }
23
+
24
+ export interface TypeConflict {
25
+ type: string;
26
+ count: number;
27
+ }
28
+
29
+ export interface FieldResult {
30
+ id: number;
31
+ path: string;
32
+ type: string;
33
+ description: string;
34
+ required: boolean;
35
+ schemaFormat: string;
36
+ messageId: string;
37
+ messageVersion: string;
38
+ messageType: string;
39
+ messageOwners?: string[];
40
+ usedInCount?: number;
41
+ conflicts?: TypeConflict[];
42
+ producers: ServiceRef[];
43
+ consumers: ServiceRef[];
44
+ }
45
+
46
+ export interface FacetEntry {
47
+ value: string;
48
+ count: number;
49
+ }
50
+
51
+ export interface QueryFieldsResult {
52
+ fields: FieldResult[];
53
+ total: number;
54
+ cursor?: string;
55
+ facets: {
56
+ formats: FacetEntry[];
57
+ types: FacetEntry[];
58
+ messageTypes: FacetEntry[];
59
+ };
60
+ }
61
+
62
+ export interface QueryFieldsParams {
63
+ q?: string;
64
+ shared?: boolean;
65
+ conflicting?: boolean;
66
+ format?: string;
67
+ type?: string;
68
+ messageType?: string;
69
+ message?: string;
70
+ producer?: string;
71
+ consumer?: string;
72
+ required?: boolean;
73
+ path?: string;
74
+ pageSize?: number;
75
+ cursor?: string;
76
+ }
77
+
78
+ const SCHEMA_SQL = `
79
+ CREATE TABLE IF NOT EXISTS fields (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ path TEXT NOT NULL,
82
+ type TEXT NOT NULL,
83
+ description TEXT NOT NULL DEFAULT '',
84
+ required INTEGER NOT NULL DEFAULT 0,
85
+ schema_format TEXT NOT NULL,
86
+ message_id TEXT NOT NULL,
87
+ message_version TEXT NOT NULL,
88
+ message_type TEXT NOT NULL,
89
+ message_name TEXT NOT NULL DEFAULT '',
90
+ message_summary TEXT NOT NULL DEFAULT '',
91
+ message_owners TEXT NOT NULL DEFAULT '[]'
92
+ );
93
+
94
+ CREATE TABLE IF NOT EXISTS message_producers (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ message_id TEXT NOT NULL,
97
+ message_version TEXT NOT NULL,
98
+ service_id TEXT NOT NULL,
99
+ service_version TEXT NOT NULL,
100
+ service_name TEXT NOT NULL DEFAULT '',
101
+ service_summary TEXT NOT NULL DEFAULT '',
102
+ service_owners TEXT NOT NULL DEFAULT '[]'
103
+ );
104
+
105
+ CREATE TABLE IF NOT EXISTS message_consumers (
106
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
107
+ message_id TEXT NOT NULL,
108
+ message_version TEXT NOT NULL,
109
+ service_id TEXT NOT NULL,
110
+ service_version TEXT NOT NULL,
111
+ service_name TEXT NOT NULL DEFAULT '',
112
+ service_summary TEXT NOT NULL DEFAULT '',
113
+ service_owners TEXT NOT NULL DEFAULT '[]'
114
+ );
115
+
116
+ CREATE INDEX IF NOT EXISTS idx_fields_path ON fields(path);
117
+ CREATE INDEX IF NOT EXISTS idx_fields_message ON fields(message_id, message_version);
118
+ CREATE INDEX IF NOT EXISTS idx_producers_message ON message_producers(message_id, message_version);
119
+ CREATE INDEX IF NOT EXISTS idx_consumers_message ON message_consumers(message_id, message_version);
120
+ `;
121
+
122
+ const FTS_SQL = `
123
+ DROP TABLE IF EXISTS fields_fts;
124
+ CREATE VIRTUAL TABLE fields_fts USING fts5(
125
+ path,
126
+ description,
127
+ type,
128
+ content=fields,
129
+ content_rowid=id
130
+ );
131
+ INSERT INTO fields_fts(rowid, path, description, type) SELECT id, path, description, type FROM fields;
132
+ `;
133
+
134
+ export class FieldsDatabase {
135
+ public db: BetterSqlite3.Database;
136
+
137
+ constructor(dbPath: string, options?: { recreate?: boolean }) {
138
+ if (options?.recreate && fs.existsSync(dbPath)) {
139
+ fs.unlinkSync(dbPath);
140
+ }
141
+ this.db = new Database(dbPath);
142
+ this.db.pragma('journal_mode = WAL');
143
+ this.db.exec(SCHEMA_SQL);
144
+ }
145
+
146
+ insertField(field: FieldRow & { messageName?: string; messageSummary?: string; messageOwners?: string[] }): void {
147
+ this.db
148
+ .prepare(
149
+ `INSERT INTO fields (path, type, description, required, schema_format, message_id, message_version, message_type, message_name, message_summary, message_owners)
150
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
151
+ )
152
+ .run(
153
+ field.path,
154
+ field.type,
155
+ field.description,
156
+ field.required ? 1 : 0,
157
+ field.schemaFormat,
158
+ field.messageId,
159
+ field.messageVersion,
160
+ field.messageType,
161
+ field.messageName || '',
162
+ field.messageSummary || '',
163
+ JSON.stringify(field.messageOwners || [])
164
+ );
165
+ }
166
+
167
+ insertProducer(
168
+ messageId: string,
169
+ messageVersion: string,
170
+ serviceId: string,
171
+ serviceVersion: string,
172
+ serviceName?: string,
173
+ serviceSummary?: string,
174
+ serviceOwners?: string[]
175
+ ): void {
176
+ this.db
177
+ .prepare(
178
+ `INSERT INTO message_producers (message_id, message_version, service_id, service_version, service_name, service_summary, service_owners)
179
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
180
+ )
181
+ .run(
182
+ messageId,
183
+ messageVersion,
184
+ serviceId,
185
+ serviceVersion,
186
+ serviceName || '',
187
+ serviceSummary || '',
188
+ JSON.stringify(serviceOwners || [])
189
+ );
190
+ }
191
+
192
+ insertConsumer(
193
+ messageId: string,
194
+ messageVersion: string,
195
+ serviceId: string,
196
+ serviceVersion: string,
197
+ serviceName?: string,
198
+ serviceSummary?: string,
199
+ serviceOwners?: string[]
200
+ ): void {
201
+ this.db
202
+ .prepare(
203
+ `INSERT INTO message_consumers (message_id, message_version, service_id, service_version, service_name, service_summary, service_owners)
204
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
205
+ )
206
+ .run(
207
+ messageId,
208
+ messageVersion,
209
+ serviceId,
210
+ serviceVersion,
211
+ serviceName || '',
212
+ serviceSummary || '',
213
+ JSON.stringify(serviceOwners || [])
214
+ );
215
+ }
216
+
217
+ rebuildFts(): void {
218
+ this.db.exec(FTS_SQL);
219
+ }
220
+
221
+ queryFields(params: QueryFieldsParams): QueryFieldsResult {
222
+ const {
223
+ q,
224
+ shared,
225
+ conflicting,
226
+ format,
227
+ type,
228
+ messageType,
229
+ message,
230
+ producer,
231
+ consumer,
232
+ required,
233
+ path: fieldPath,
234
+ pageSize = 50,
235
+ cursor,
236
+ } = params;
237
+
238
+ const conditions: string[] = [];
239
+ const bindings: any[] = [];
240
+
241
+ // Exact field path filter
242
+ if (fieldPath) {
243
+ conditions.push(`f.path = ?`);
244
+ bindings.push(fieldPath);
245
+ }
246
+
247
+ // FTS filter (quote the term so dots/brackets in field paths aren't parsed as FTS syntax)
248
+ if (q) {
249
+ conditions.push(`f.id IN (SELECT rowid FROM fields_fts WHERE fields_fts MATCH ?)`);
250
+ const escaped = q.replace(/"/g, '""');
251
+ bindings.push(`"${escaped}" *`);
252
+ }
253
+
254
+ // Facet filters (support comma-separated multi-select)
255
+ if (format) {
256
+ const formats = format
257
+ .split(',')
258
+ .map((f) => f.trim())
259
+ .filter(Boolean);
260
+ conditions.push(`f.schema_format IN (${formats.map(() => '?').join(', ')})`);
261
+ bindings.push(...formats);
262
+ }
263
+ if (type) {
264
+ conditions.push(`f.type = ?`);
265
+ bindings.push(type);
266
+ }
267
+ if (messageType) {
268
+ const types = messageType
269
+ .split(',')
270
+ .map((t) => t.trim())
271
+ .filter(Boolean);
272
+ conditions.push(`f.message_type IN (${types.map(() => '?').join(', ')})`);
273
+ bindings.push(...types);
274
+ }
275
+ if (message) {
276
+ conditions.push(`f.message_id = ?`);
277
+ bindings.push(message);
278
+ }
279
+ if (required) {
280
+ conditions.push(`f.required = 1`);
281
+ }
282
+ if (producer) {
283
+ conditions.push(
284
+ `EXISTS (SELECT 1 FROM message_producers p WHERE p.message_id = f.message_id AND p.message_version = f.message_version AND p.service_id = ?)`
285
+ );
286
+ bindings.push(producer);
287
+ }
288
+ if (consumer) {
289
+ conditions.push(
290
+ `EXISTS (SELECT 1 FROM message_consumers c WHERE c.message_id = f.message_id AND c.message_version = f.message_version AND c.service_id = ?)`
291
+ );
292
+ bindings.push(consumer);
293
+ }
294
+
295
+ // Shared fields filter: only fields whose path appears in more than one distinct message
296
+ if (shared) {
297
+ const sharedSubquery = `SELECT path FROM fields GROUP BY path HAVING COUNT(DISTINCT message_id || '/' || message_version) > 1`;
298
+ conditions.push(`f.path IN (${sharedSubquery})`);
299
+ }
300
+
301
+ // Conflicting fields filter: only fields whose path has multiple distinct types
302
+ if (conflicting) {
303
+ const conflictSubquery = `SELECT path FROM fields GROUP BY path HAVING COUNT(DISTINCT type) > 1`;
304
+ conditions.push(`f.path IN (${conflictSubquery})`);
305
+ }
306
+
307
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
308
+
309
+ // Cursor-based pagination
310
+ const cursorConditions: string[] = [];
311
+ const cursorBindings: any[] = [];
312
+ if (cursor) {
313
+ const lastId = decodeCursor(cursor);
314
+ cursorConditions.push(`f.id > ?`);
315
+ cursorBindings.push(lastId);
316
+ }
317
+
318
+ const paginationWhere =
319
+ cursorConditions.length > 0
320
+ ? whereClause
321
+ ? `${whereClause} AND ${cursorConditions.join(' AND ')}`
322
+ : `WHERE ${cursorConditions.join(' AND ')}`
323
+ : whereClause;
324
+
325
+ // Total count (without pagination)
326
+ const countSql = `SELECT COUNT(*) as cnt FROM fields f ${whereClause}`;
327
+ const total = (this.db.prepare(countSql).get(...bindings) as any).cnt;
328
+
329
+ // Main query with pagination
330
+ const mainSql = `SELECT f.* FROM fields f ${paginationWhere} ORDER BY f.id ASC LIMIT ?`;
331
+ const allBindings = [...bindings, ...cursorBindings, pageSize];
332
+ const rows = this.db.prepare(mainSql).all(...allBindings) as any[];
333
+
334
+ // Prepare usedInCount query (distinct messages per field path)
335
+ const usedInStmt = this.db.prepare(
336
+ `SELECT COUNT(DISTINCT message_id || '/' || message_version) as cnt FROM fields WHERE path = ?`
337
+ );
338
+
339
+ // Prepare conflicts query (distinct types per field path with counts)
340
+ const conflictsStmt = this.db.prepare(
341
+ `SELECT type, COUNT(DISTINCT message_id || '/' || message_version) as count FROM fields WHERE path = ? GROUP BY type`
342
+ );
343
+
344
+ // Gather producers and consumers for the returned fields
345
+ const fields: FieldResult[] = rows.map((row) => {
346
+ const producers = this.db
347
+ .prepare(
348
+ `SELECT service_id, service_version, service_name, service_summary, service_owners FROM message_producers WHERE message_id = ? AND message_version = ?`
349
+ )
350
+ .all(row.message_id, row.message_version) as any[];
351
+
352
+ const consumers = this.db
353
+ .prepare(
354
+ `SELECT service_id, service_version, service_name, service_summary, service_owners FROM message_consumers WHERE message_id = ? AND message_version = ?`
355
+ )
356
+ .all(row.message_id, row.message_version) as any[];
357
+
358
+ const parseOwners = (raw: string) => {
359
+ try {
360
+ return JSON.parse(raw || '[]');
361
+ } catch {
362
+ return [];
363
+ }
364
+ };
365
+
366
+ const usedInCount = (usedInStmt.get(row.path) as any).cnt;
367
+ const typeRows = conflictsStmt.all(row.path) as any[];
368
+ const conflicts =
369
+ typeRows.length > 1 ? typeRows.map((r) => ({ type: r.type as string, count: r.count as number })) : undefined;
370
+
371
+ return {
372
+ id: row.id,
373
+ path: row.path,
374
+ type: row.type,
375
+ description: row.description,
376
+ required: row.required === 1,
377
+ schemaFormat: row.schema_format,
378
+ messageId: row.message_id,
379
+ messageVersion: row.message_version,
380
+ messageType: row.message_type,
381
+ messageName: row.message_name || row.message_id,
382
+ messageSummary: row.message_summary || '',
383
+ messageOwners: parseOwners(row.message_owners),
384
+ usedInCount,
385
+ conflicts,
386
+ producers: producers.map((p) => ({
387
+ id: p.service_id,
388
+ version: p.service_version,
389
+ name: p.service_name || p.service_id,
390
+ summary: p.service_summary || '',
391
+ owners: parseOwners(p.service_owners),
392
+ })),
393
+ consumers: consumers.map((c) => ({
394
+ id: c.service_id,
395
+ version: c.service_version,
396
+ name: c.service_name || c.service_id,
397
+ summary: c.service_summary || '',
398
+ owners: parseOwners(c.service_owners),
399
+ })),
400
+ };
401
+ });
402
+
403
+ // Facets (computed from filtered set, not paginated)
404
+ const formatsFacetSql = `SELECT f.schema_format as value, COUNT(*) as count FROM fields f ${whereClause} GROUP BY f.schema_format`;
405
+ const formats = this.db.prepare(formatsFacetSql).all(...bindings) as FacetEntry[];
406
+
407
+ const typesFacetSql = `SELECT f.type as value, COUNT(*) as count FROM fields f ${whereClause} GROUP BY f.type`;
408
+ const types = this.db.prepare(typesFacetSql).all(...bindings) as FacetEntry[];
409
+
410
+ const messageTypesFacetSql = `SELECT f.message_type as value, COUNT(*) as count FROM fields f ${whereClause} GROUP BY f.message_type`;
411
+ const messageTypes = this.db.prepare(messageTypesFacetSql).all(...bindings) as FacetEntry[];
412
+
413
+ // Build cursor for next page
414
+ const lastRow = rows[rows.length - 1];
415
+ const nextCursor = lastRow && rows.length === pageSize ? encodeCursor(lastRow.id) : undefined;
416
+
417
+ return {
418
+ fields,
419
+ total,
420
+ cursor: nextCursor,
421
+ facets: { formats, types, messageTypes },
422
+ };
423
+ }
424
+
425
+ close(): void {
426
+ this.db.close();
427
+ }
428
+ }
429
+
430
+ function encodeCursor(id: number): string {
431
+ return Buffer.from(String(id)).toString('base64url');
432
+ }
433
+
434
+ function decodeCursor(cursor: string): number {
435
+ return parseInt(Buffer.from(cursor, 'base64url').toString(), 10);
436
+ }
437
+
438
+ // Singleton management
439
+ let instance: FieldsDatabase | null = null;
440
+
441
+ export function getFieldsDatabase(dbPath: string): FieldsDatabase {
442
+ if (!instance) {
443
+ instance = new FieldsDatabase(dbPath);
444
+ }
445
+ return instance;
446
+ }
447
+
448
+ export function closeFieldsDatabase(): void {
449
+ if (instance) {
450
+ instance.close();
451
+ instance = null;
452
+ }
453
+ }
@@ -0,0 +1,43 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { Hono } from 'hono';
3
+ import { getFieldsDatabase } from '@enterprise/fields/fields-db';
4
+ import path from 'node:path';
5
+
6
+ const catalogDirectory = process.env.CATALOG_DIR || process.cwd();
7
+ const dbPath = path.join(catalogDirectory, '.eventcatalog', 'fields.db');
8
+
9
+ const app = new Hono().basePath('/api/schemas/fields');
10
+
11
+ app.get('/', async (c) => {
12
+ try {
13
+ const url = new URL(c.req.raw.url);
14
+ const sp = url.searchParams;
15
+
16
+ const db = getFieldsDatabase(dbPath);
17
+ const params = {
18
+ q: sp.get('q') || undefined,
19
+ format: sp.get('format') || undefined,
20
+ messageType: sp.get('messageType') || undefined,
21
+ message: sp.get('message') || undefined,
22
+ producer: sp.get('producer') || undefined,
23
+ consumer: sp.get('consumer') || undefined,
24
+ required: sp.get('required') === 'true',
25
+ shared: sp.get('shared') === 'true',
26
+ conflicting: sp.get('conflicting') === 'true',
27
+ cursor: sp.get('cursor') || undefined,
28
+ pageSize: sp.get('pageSize') ? parseInt(sp.get('pageSize')!, 10) : undefined,
29
+ path: sp.get('path') || undefined,
30
+ };
31
+
32
+ const result = db.queryFields(params);
33
+ return c.json(result);
34
+ } catch (err: any) {
35
+ return c.json({ error: `Failed to query fields: ${err.message}` }, 500);
36
+ }
37
+ });
38
+
39
+ export const ALL: APIRoute = async ({ request }) => {
40
+ return app.fetch(request);
41
+ };
42
+
43
+ export const prerender = false;
@@ -0,0 +1,19 @@
1
+ ---
2
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
3
+ import FieldsExplorer from '@components/FieldsExplorer/FieldsExplorer';
4
+ import { isEventCatalogScaleEnabled } from '@utils/feature';
5
+
6
+ export const prerender = false;
7
+ ---
8
+
9
+ <VerticalSideBarLayout title="Fields Explorer - EventCatalog" showNestedSideBar={false}>
10
+ <main class="flex docs-layout h-[calc(100vh-var(--header-height,0px)-64px)] bg-[rgb(var(--ec-page-bg))]">
11
+ <div class="flex docs-layout w-full h-full">
12
+ <div class="w-full flex flex-col h-full">
13
+ <div class="w-full max-w-none! h-full flex flex-col overflow-hidden">
14
+ <FieldsExplorer client:load isScaleEnabled={isEventCatalogScaleEnabled()} />
15
+ </div>
16
+ </div>
17
+ </div>
18
+ </main>
19
+ </VerticalSideBarLayout>
@@ -16,6 +16,7 @@ import {
16
16
  FileText,
17
17
  SquareDashedMousePointerIcon,
18
18
  Braces,
19
+ Waypoints,
19
20
  } from 'lucide-react';
20
21
  import BaseLayout from './BaseLayout.astro';
21
22
  import Header from '../components/Header.astro';
@@ -51,7 +52,13 @@ import { buildUrl } from '@utils/url-builder';
51
52
  import { getQueries } from '@utils/collections/queries';
52
53
  import { hasLandingPageForDocs } from '@utils/pages';
53
54
 
54
- import { isEventCatalogUpgradeEnabled, isEmbedEnabled, isCustomStylesEnabled } from '@utils/feature';
55
+ import {
56
+ isEventCatalogUpgradeEnabled,
57
+ isEmbedEnabled,
58
+ isCustomStylesEnabled,
59
+ isEventCatalogScaleEnabled,
60
+ isSSR,
61
+ } from '@utils/feature';
55
62
  import { getUsers } from '@utils/collections/users';
56
63
  import { getTeams } from '@utils/collections/teams';
57
64
 
@@ -132,7 +139,9 @@ const navigationItems = [
132
139
  (currentPath.includes('/docs') && !currentPath.includes('/docs/custom')) ||
133
140
  currentPath.includes('/architecture/') ||
134
141
  currentPath.includes('/visualiser') ||
135
- (currentPath.includes('/schemas') && !currentPath.includes('/schemas/explorer')),
142
+ (currentPath.includes('/schemas') &&
143
+ !currentPath.includes('/schemas/explorer') &&
144
+ !currentPath.includes('/schemas/fields')),
136
145
  },
137
146
  {
138
147
  id: '/discover',
@@ -146,8 +155,19 @@ const navigationItems = [
146
155
  label: 'Schema Explorer',
147
156
  icon: Braces,
148
157
  href: buildUrl('/schemas/explorer'),
149
- current: currentPath.includes('/schemas/explorer'),
158
+ current: currentPath.includes('/schemas/explorer') && !currentPath.includes('/schemas/fields'),
150
159
  },
160
+ ...(isSSR()
161
+ ? [
162
+ {
163
+ id: '/schemas/fields',
164
+ label: 'Schema Fields',
165
+ icon: Waypoints,
166
+ href: buildUrl('/schemas/fields'),
167
+ current: currentPath.includes('/schemas/fields'),
168
+ },
169
+ ]
170
+ : []),
151
171
  {
152
172
  id: '/directory',
153
173
  label: 'Users & Teams',
@@ -36,17 +36,16 @@ if (isRemote) {
36
36
  const pageTitle = `${collection} | ${data.name} | GraphQL Schema`.replace(/^\w/, (c) => c.toUpperCase());
37
37
 
38
38
  const getServiceBadge = () => {
39
- return [{ backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon, class: 'text-pink-600' }];
39
+ return [{ backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon }];
40
40
  };
41
41
 
42
42
  const getGraphQLBadge = () => {
43
43
  return [
44
44
  {
45
- backgroundColor: 'white',
46
- textColor: 'gray',
45
+ backgroundColor: 'purple',
46
+ textColor: 'purple',
47
47
  content: 'GraphQL Schema',
48
48
  iconURL: buildUrl('/icons/graphql.svg', true),
49
- class: 'text-black',
50
49
  id: 'graphql-schema-badge',
51
50
  },
52
51
  ];
@@ -68,15 +67,15 @@ const pagefindAttributes =
68
67
  <main class="flex sm:px-8 docs-layout h-full" {...pagefindAttributes}>
69
68
  <div class="flex docs-layout w-full">
70
69
  <div class="w-full lg:mr-2 pr-8 overflow-y-auto py-8">
71
- <div class="border-b border-gray-200 md:pb-4">
70
+ <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-4">
72
71
  <div>
73
72
  <div class="flex justify-between items-start">
74
73
  <div class="flex-1">
75
- <h1 class="text-2xl md:text-4xl font-bold text-black mb-2">
74
+ <h1 class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))] mb-2">
76
75
  {data.name}
77
- <span class="text-gray-900">(v{data.version})</span>
76
+ <span class="text-[rgb(var(--ec-page-text-muted))]">(v{data.version})</span>
78
77
  </h1>
79
- <h2 class="text-lg text-gray-600 font-medium mb-1">GraphQL Schema</h2>
78
+ <h2 class="text-lg text-[rgb(var(--ec-page-text-muted))] font-medium mb-1">GraphQL Schema</h2>
80
79
  </div>
81
80
  </div>
82
81
 
@@ -88,13 +87,12 @@ const pagefindAttributes =
88
87
  id={badge.id || ''}
89
88
  class={`
90
89
  inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
91
- bg-${badge.backgroundColor || 'white'}-50 border border-${badge.backgroundColor || 'gray'}-200
92
- text-${badge.textColor || 'gray'}-700 shadow-xs
93
- transition-all duration-200 ease-out
94
- ${badge.class ? badge.class : ''}
90
+ bg-${badge.backgroundColor || 'gray'}-500/10 border border-${badge.backgroundColor || 'gray'}-500/20
91
+ text-${badge.textColor || 'gray'}-500
92
+ shadow-xs transition-all duration-200 ease-out
95
93
  `}
96
94
  >
97
- {badge.icon && <badge.icon className={`w-4 h-4 flex-shrink-0 text-${badge.textColor || 'gray'}-600`} />}
95
+ {badge.icon && <badge.icon className={`w-4 h-4 flex-shrink-0`} />}
98
96
  {badge.iconURL && <img src={badge.iconURL} class="w-4 h-4 flex-shrink-0 opacity-80" alt="" />}
99
97
  <span>{badge.content}</span>
100
98
  </span>
@@ -151,12 +149,12 @@ const pagefindAttributes =
151
149
  {
152
150
  fileExists && content && (
153
151
  <div class="mt-6">
154
- <div class="bg-gray-50 rounded-lg p-4 mb-4">
152
+ <div class="bg-[rgb(var(--ec-card-bg))] rounded-lg p-4 mb-4 border border-[rgb(var(--ec-page-border))]">
155
153
  <div class="flex items-center gap-2 mb-2">
156
154
  <img src={buildUrl('/icons/graphql.svg', true)} class="w-5 h-5" alt="GraphQL" />
157
- <h3 class="text-lg font-semibold text-gray-800">GraphQL Schema</h3>
155
+ <h3 class="text-lg font-semibold text-[rgb(var(--ec-page-text))]">GraphQL Schema</h3>
158
156
  </div>
159
- <p class="text-sm text-gray-600">
157
+ <p class="text-sm text-[rgb(var(--ec-page-text-muted))]">
160
158
  This schema defines the GraphQL API structure including types, queries, mutations, and subscriptions for{' '}
161
159
  {data.name}.
162
160
  </p>