@eventcatalog/core 3.46.1 → 3.47.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 (38) 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-VZ6L3G2S.js → chunk-6GSS7Q6C.js} +1 -1
  6. package/dist/{chunk-TZ72NP2W.js → chunk-KCQPWC27.js} +1 -1
  7. package/dist/{chunk-E4QGTJXO.js → chunk-TL2SVMED.js} +1 -1
  8. package/dist/{chunk-A7XUEEUA.js → chunk-UKMUMDL5.js} +1 -1
  9. package/dist/{chunk-62QAJWPT.js → chunk-X5CVZQHR.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  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/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +14 -2
  19. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +63 -0
  20. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.ts +25 -7
  21. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +532 -0
  22. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +6 -0
  23. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +20 -2
  24. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +14 -5
  25. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +8 -2
  26. package/eventcatalog/src/components/SchemaExplorer/utils.ts +4 -0
  27. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +19 -4
  28. package/eventcatalog/src/pages/docs/[type]/[id]/language.mdx.ts +16 -5
  29. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +4 -2
  30. package/eventcatalog/src/stores/sidebar-store/builders/entity.ts +87 -0
  31. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +1 -0
  32. package/eventcatalog/src/stores/sidebar-store/state.ts +15 -14
  33. package/eventcatalog/src/utils/collections/domains.ts +39 -0
  34. package/eventcatalog/src/utils/collections/entities.ts +3 -1
  35. package/eventcatalog/src/utils/files.ts +9 -0
  36. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +24 -12
  37. package/eventcatalog/src/utils/protobuf-schema.ts +476 -0
  38. package/package.json +1 -1
@@ -2,6 +2,7 @@ import * as Dialog from '@radix-ui/react-dialog';
2
2
  import { XMarkIcon, ArrowsPointingOutIcon } from '@heroicons/react/24/outline';
3
3
  import JSONSchemaViewer from './JSONSchemaViewer';
4
4
  import AvroSchemaViewer from './AvroSchemaViewer';
5
+ import ProtobufSchemaViewer from './ProtobufSchemaViewer';
5
6
  import type { SchemaItem } from './types';
6
7
 
7
8
  interface SchemaViewerModalProps {
@@ -10,6 +11,7 @@ interface SchemaViewerModalProps {
10
11
  message: SchemaItem;
11
12
  parsedSchema: any;
12
13
  parsedAvroSchema?: any;
14
+ parsedProtoSchema?: any;
13
15
  }
14
16
 
15
17
  export default function SchemaViewerModal({
@@ -18,10 +20,12 @@ export default function SchemaViewerModal({
18
20
  message,
19
21
  parsedSchema,
20
22
  parsedAvroSchema,
23
+ parsedProtoSchema,
21
24
  }: SchemaViewerModalProps) {
22
- if (!parsedSchema && !parsedAvroSchema) return null;
25
+ if (!parsedSchema && !parsedAvroSchema && !parsedProtoSchema) return null;
23
26
 
24
27
  const isAvro = !!parsedAvroSchema;
28
+ const isProto = !!parsedProtoSchema;
25
29
 
26
30
  return (
27
31
  <Dialog.Root open={isOpen} onOpenChange={onOpenChange}>
@@ -35,7 +39,7 @@ export default function SchemaViewerModal({
35
39
  <div>
36
40
  <Dialog.Title className="text-xl font-semibold text-[rgb(var(--ec-page-text))]">{message.data.name}</Dialog.Title>
37
41
  <Dialog.Description className="text-sm text-[rgb(var(--ec-page-text-muted))] mt-1">
38
- v{message.data.version} · {isAvro ? 'Avro' : 'JSON'} Schema
42
+ v{message.data.version} · {isAvro ? 'Avro' : isProto ? 'Protobuf' : 'JSON'} Schema
39
43
  </Dialog.Description>
40
44
  </div>
41
45
  </div>
@@ -54,6 +58,8 @@ export default function SchemaViewerModal({
54
58
  <div className="flex-1 overflow-hidden p-6">
55
59
  {isAvro ? (
56
60
  <AvroSchemaViewer schema={parsedAvroSchema} expand={true} search={true} />
61
+ ) : isProto ? (
62
+ <ProtobufSchemaViewer schema={parsedProtoSchema} expand={true} search={true} />
57
63
  ) : (
58
64
  <JSONSchemaViewer schema={parsedSchema} expand={true} search={true} />
59
65
  )}
@@ -7,6 +7,7 @@ export const ICON_SPECS: Record<string, string> = {
7
7
  avro: 'avro',
8
8
  avsc: 'avro',
9
9
  proto: 'proto',
10
+ protobuf: 'proto',
10
11
  json: 'json-schema',
11
12
  };
12
13
 
@@ -20,6 +21,7 @@ export function getFormatBadge(ext?: string): { label: string; color: string } {
20
21
  case 'avsc':
21
22
  return { label: 'avro', color: 'text-blue-400' };
22
23
  case 'proto':
24
+ case 'protobuf':
23
25
  return { label: 'proto', color: 'text-orange-400' };
24
26
  case 'yaml':
25
27
  case 'yml':
@@ -48,6 +50,7 @@ export const getLanguageForHighlight = (extension?: string): string => {
48
50
  case 'json':
49
51
  return 'json';
50
52
  case 'proto':
53
+ case 'protobuf':
51
54
  return 'protobuf';
52
55
  case 'xsd':
53
56
  case 'xml':
@@ -79,6 +82,7 @@ export const getSchemaTypeLabel = (extension?: string): string => {
79
82
  case 'avsc':
80
83
  return 'Avro';
81
84
  case 'proto':
85
+ case 'protobuf':
82
86
  return 'Protobuf';
83
87
  case 'xsd':
84
88
  return 'XML Schema';
@@ -10,10 +10,19 @@ export class Page extends HybridPage {
10
10
  return [];
11
11
  }
12
12
 
13
- const { getDomains } = await import('@utils/collections/domains');
13
+ const { getDomains, hasUbiquitousLanguageTermsWithSubdomains } = await import('@utils/collections/domains');
14
14
  const domains = await getDomains({ getAllVersions: false });
15
+ const domainsWithUbiquitousLanguage = await domains.reduce<Promise<typeof domains>>(async (acc, domain) => {
16
+ const accumulator = await acc;
15
17
 
16
- return domains.map((item) => ({
18
+ if (await hasUbiquitousLanguageTermsWithSubdomains(domain)) {
19
+ return [...accumulator, domain];
20
+ }
21
+
22
+ return accumulator;
23
+ }, Promise.resolve([]));
24
+
25
+ return domainsWithUbiquitousLanguage.map((item) => ({
17
26
  params: {
18
27
  type: item.collection,
19
28
  id: item.data.id,
@@ -23,9 +32,15 @@ export class Page extends HybridPage {
23
32
  }
24
33
 
25
34
  protected static async fetchData(params: any) {
26
- const { getDomains } = await import('@utils/collections/domains');
35
+ const { getDomains, hasUbiquitousLanguageTermsWithSubdomains } = await import('@utils/collections/domains');
27
36
  const domains = await getDomains({ getAllVersions: false });
28
- return domains.find((d) => d.data.id === params.id && d.collection === params.type) || null;
37
+ const domain = domains.find((d) => d.data.id === params.id && d.collection === params.type);
38
+
39
+ if (!domain || !(await hasUbiquitousLanguageTermsWithSubdomains(domain))) {
40
+ return null;
41
+ }
42
+
43
+ return domain;
29
44
  }
30
45
 
31
46
  protected static createNotFoundResponse(): Response {
@@ -1,4 +1,4 @@
1
- import { getDomains, getUbiquitousLanguage } from '@utils/collections/domains';
1
+ import { getDomains, getUbiquitousLanguage, hasUbiquitousLanguageTerms } from '@utils/collections/domains';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import type { APIRoute } from 'astro';
4
4
  import config from '@config';
@@ -9,8 +9,19 @@ import { filterMarkdownForAgents } from '@utils/llms';
9
9
  export async function getStaticPaths() {
10
10
  const domains = await getDomains({ getAllVersions: false });
11
11
 
12
- const buildPages = (collection: CollectionEntry<'domains'>[]) => {
13
- return collection.map((item) => ({
12
+ const buildPages = async (collection: CollectionEntry<'domains'>[]) => {
13
+ const collectionWithUbiquitousLanguage = await collection.reduce<Promise<CollectionEntry<'domains'>[]>>(async (acc, item) => {
14
+ const accumulator = await acc;
15
+ const ubiquitousLanguages = await getUbiquitousLanguage(item);
16
+
17
+ if (ubiquitousLanguages.some(hasUbiquitousLanguageTerms)) {
18
+ return [...accumulator, item];
19
+ }
20
+
21
+ return accumulator;
22
+ }, Promise.resolve([]));
23
+
24
+ return collectionWithUbiquitousLanguage.map((item) => ({
14
25
  params: {
15
26
  type: item.collection,
16
27
  id: item.data.id,
@@ -22,7 +33,7 @@ export async function getStaticPaths() {
22
33
  }));
23
34
  };
24
35
 
25
- return [...buildPages(domains)];
36
+ return [...(await buildPages(domains))];
26
37
  }
27
38
 
28
39
  export const GET: APIRoute = async ({ params, props }) => {
@@ -40,7 +51,7 @@ export const GET: APIRoute = async ({ params, props }) => {
40
51
  const ubiquitousLanguages = await getUbiquitousLanguage(domain as CollectionEntry<'domains'>);
41
52
  const ubiquitousLanguage = ubiquitousLanguages[0];
42
53
 
43
- if (ubiquitousLanguage?.filePath) {
54
+ if (ubiquitousLanguage?.filePath && hasUbiquitousLanguageTerms(ubiquitousLanguage)) {
44
55
  let file = filterMarkdownForAgents(fs.readFileSync(ubiquitousLanguage.filePath, 'utf8'));
45
56
 
46
57
  return new Response(file, { status: 200 });
@@ -13,7 +13,7 @@ import {
13
13
  } from './shared';
14
14
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
15
15
  import { pluralizeMessageType } from '@utils/collections/messages';
16
- import { getSpecificationsForDomain } from '@utils/collections/domains';
16
+ import { getSpecificationsForDomain, hasUbiquitousLanguageTermsWithSubdomainsInCollection } from '@utils/collections/domains';
17
17
  import { iconFieldsForResource } from '@utils/icon';
18
18
 
19
19
  export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[], context: ResourceGroupContext): NavNode => {
@@ -41,7 +41,9 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
41
41
  const resourceGroups = domain.data.resourceGroups || [];
42
42
  const hasResourceGroups = resourceGroups.length > 0;
43
43
 
44
- const renderUbiquitousLanguage = shouldRenderSideBarSection(domain, 'ubiquitousLanguage');
44
+ const renderUbiquitousLanguage =
45
+ hasUbiquitousLanguageTermsWithSubdomainsInCollection(domain, context.ubiquitousLanguages || []) &&
46
+ shouldRenderSideBarSection(domain, 'ubiquitousLanguage');
45
47
  const renderOwners = owners.length > 0 && shouldRenderSideBarSection(domain, 'owners');
46
48
 
47
49
  const renderVisualiser = isVisualiserEnabled();
@@ -0,0 +1,87 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+ import { buildUrl } from '@utils/url-builder';
3
+ import type { NavNode, ChildRef, ResourceGroupContext } from './shared';
4
+ import {
5
+ buildAttachmentsSection,
6
+ buildOwnersSection,
7
+ buildQuickReferenceSection,
8
+ buildResourceDocsSection,
9
+ shouldRenderSideBarSection,
10
+ } from './shared';
11
+ import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature';
12
+ import { iconFieldsForResource } from '@utils/icon';
13
+
14
+ export const buildEntityNode = (entity: CollectionEntry<'entities'>, owners: any[], context: ResourceGroupContext): NavNode => {
15
+ const domains = entity.data.domains || [];
16
+ const services = entity.data.services || [];
17
+
18
+ const entityMapTargets = [
19
+ domains.length === 1 && {
20
+ label: 'Domain',
21
+ href: buildUrl(`/visualiser/domains/${(domains[0] as any).data.id}/${(domains[0] as any).data.version}/entity-map`),
22
+ },
23
+ services.length === 1 && {
24
+ label: 'Service',
25
+ href: buildUrl(`/visualiser/services/${(services[0] as any).data.id}/${(services[0] as any).data.version}/entity-map`),
26
+ },
27
+ ].filter(Boolean) as { label: string; href: string }[];
28
+
29
+ const renderArchitecture = isVisualiserEnabled() && entityMapTargets.length > 0;
30
+ const renderDomains = domains.length > 0 && shouldRenderSideBarSection(entity, 'domains');
31
+ const renderServices = services.length > 0 && shouldRenderSideBarSection(entity, 'services');
32
+ const renderOwners = owners.length > 0 && shouldRenderSideBarSection(entity, 'owners');
33
+ const hasAttachments = entity.data.attachments && entity.data.attachments.length > 0;
34
+
35
+ const docsSection = buildResourceDocsSection(
36
+ 'entities',
37
+ entity.data.id,
38
+ entity.data.version,
39
+ context.resourceDocs,
40
+ context.resourceDocCategories
41
+ );
42
+
43
+ return {
44
+ type: 'item',
45
+ title: entity.data.name,
46
+ badge: 'Entity',
47
+ summary: entity.data.summary,
48
+ ...iconFieldsForResource(entity.data, 'Box'),
49
+ pages: [
50
+ buildQuickReferenceSection(
51
+ [
52
+ { title: 'Overview', href: buildUrl(`/docs/entities/${entity.data.id}/${entity.data.version}`) },
53
+ isChangelogEnabled() &&
54
+ shouldRenderSideBarSection(entity, 'changelog') && {
55
+ title: 'Changelog',
56
+ href: buildUrl(`/docs/entities/${entity.data.id}/${entity.data.version}/changelog`),
57
+ },
58
+ ].filter(Boolean) as { title: string; href: string }[]
59
+ ),
60
+ docsSection,
61
+ renderArchitecture && {
62
+ type: 'group',
63
+ title: 'Architecture',
64
+ icon: 'Workflow',
65
+ pages: entityMapTargets.map((target) => ({
66
+ type: 'item',
67
+ title: entityMapTargets.length === 1 ? 'Entity Map' : `${target.label} Entity Map`,
68
+ href: target.href,
69
+ })),
70
+ },
71
+ renderDomains && {
72
+ type: 'group',
73
+ title: 'Domains',
74
+ icon: 'Boxes',
75
+ pages: domains.map((domain: any) => `domain:${domain.data.id}:${domain.data.version}`),
76
+ },
77
+ renderServices && {
78
+ type: 'group',
79
+ title: 'Services',
80
+ icon: 'Server',
81
+ pages: services.map((service: any) => `service:${service.data.id}:${service.data.version}`),
82
+ },
83
+ renderOwners && buildOwnersSection(owners),
84
+ hasAttachments && buildAttachmentsSection(entity.data.attachments as any[]),
85
+ ].filter(Boolean) as ChildRef[],
86
+ };
87
+ };
@@ -66,6 +66,7 @@ export type ResourceGroupContext = {
66
66
  containers: CollectionEntry<'containers'>[];
67
67
  entities?: CollectionEntry<'entities'>[];
68
68
  dataProducts: CollectionEntry<'data-products'>[];
69
+ ubiquitousLanguages?: CollectionEntry<'ubiquitousLanguages'>[];
69
70
  diagrams: CollectionEntry<'diagrams'>[];
70
71
  schemas?: CollectionEntry<'schemas'>[];
71
72
  adrs: Adr[];
@@ -25,6 +25,7 @@ import { buildContainerNode } from './builders/container';
25
25
  import { buildFlowNode } from './builders/flow';
26
26
  import { buildDataProductNode } from './builders/data-product';
27
27
  import { buildAdrNode } from './builders/adr';
28
+ import { buildEntityNode } from './builders/entity';
28
29
  import config from '@config';
29
30
  import { getDesigns } from '@utils/collections/designs';
30
31
  import { getChannels } from '@utils/collections/channels';
@@ -293,6 +294,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
293
294
  entities,
294
295
  adrs,
295
296
  schemas,
297
+ ubiquitousLanguages,
296
298
  resourceDocs,
297
299
  resourceDocCategories,
298
300
  ] = await Promise.all([
@@ -311,6 +313,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
311
313
  getEntities({ getAllVersions: false }),
312
314
  getAdrs({ getAllVersions: false }),
313
315
  getCollection('schemas'),
316
+ getCollection('ubiquitousLanguages'),
314
317
  getResourceDocs(),
315
318
  getResourceDocCategories(),
316
319
  ]);
@@ -333,6 +336,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
333
336
  channels,
334
337
  diagrams,
335
338
  schemas,
339
+ ubiquitousLanguages,
336
340
  dataProducts,
337
341
  entities,
338
342
  adrs,
@@ -406,6 +410,14 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
406
410
  })
407
411
  );
408
412
 
413
+ const entitiesWithOwners = await Promise.all(
414
+ entities.map(async (entity) => {
415
+ const owners = await Promise.all((entity.data.owners || []).map((owner) => getOwner(owner)));
416
+ const filteredOwners = owners.filter((o) => o !== undefined) as Array<NonNullable<(typeof owners)[0]>>;
417
+ return { entity, owners: filteredOwners };
418
+ })
419
+ );
420
+
409
421
  const flowNodes = flows.reduce(
410
422
  (acc, flow) => {
411
423
  acc[`flow:${flow.data.id}:${flow.data.version}`] = withArchitectureDecisionsSection(
@@ -650,21 +662,10 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
650
662
  {} as Record<string, NavNode | string>
651
663
  );
652
664
 
653
- const entityNodes = entities.reduce(
654
- (acc, entity) => {
665
+ const entityNodes = entitiesWithOwners.reduce(
666
+ (acc, { entity, owners }) => {
655
667
  const versionedKey = `entity:${entity.data.id}:${entity.data.version}`;
656
- acc[versionedKey] = withArchitectureDecisionsSection(
657
- {
658
- type: 'item',
659
- title: entity.data.name,
660
- badge: 'Entity',
661
- summary: entity.data.summary,
662
- icon: 'Box',
663
- href: buildUrl(`/docs/entities/${entity.data.id}/${entity.data.version}`),
664
- },
665
- entity,
666
- adrs
667
- );
668
+ acc[versionedKey] = withArchitectureDecisionsSection(buildEntityNode(entity, owners, context), entity, adrs);
668
669
  if (entity.data.latestVersion === entity.data.version) {
669
670
  acc[`entity:${entity.data.id}`] = versionedKey;
670
671
  }
@@ -367,6 +367,36 @@ export const getUbiquitousLanguage = async (domain: Domain): Promise<UbiquitousL
367
367
  return ubiquitousLanguages;
368
368
  };
369
369
 
370
+ export const hasUbiquitousLanguageTerms = (ubiquitousLanguage: UbiquitousLanguage | null | undefined): boolean =>
371
+ (ubiquitousLanguage?.data?.dictionary?.length || 0) > 0;
372
+
373
+ export const getUbiquitousLanguageFromCollection = (
374
+ domain: Domain,
375
+ ubiquitousLanguages: UbiquitousLanguage[]
376
+ ): UbiquitousLanguage[] => {
377
+ const domainFolder = path.dirname(domain.filePath || '');
378
+
379
+ return ubiquitousLanguages.filter((ubiquitousLanguage) => {
380
+ const ubiquitousLanguageFolder = path.dirname(ubiquitousLanguage.filePath || '');
381
+ return domainFolder === ubiquitousLanguageFolder;
382
+ });
383
+ };
384
+
385
+ export const hasUbiquitousLanguageTermsInCollection = (domain: Domain, ubiquitousLanguages: UbiquitousLanguage[]): boolean =>
386
+ getUbiquitousLanguageFromCollection(domain, ubiquitousLanguages).some(hasUbiquitousLanguageTerms);
387
+
388
+ export const hasUbiquitousLanguageTermsWithSubdomainsInCollection = (
389
+ domain: Domain,
390
+ ubiquitousLanguages: UbiquitousLanguage[]
391
+ ): boolean => {
392
+ const subdomains = (domain.data.domains as unknown as Domain[]) || [];
393
+
394
+ return (
395
+ hasUbiquitousLanguageTermsInCollection(domain, ubiquitousLanguages) ||
396
+ subdomains.some((subdomain) => hasUbiquitousLanguageTermsInCollection(subdomain, ubiquitousLanguages))
397
+ );
398
+ };
399
+
370
400
  export const getUbiquitousLanguageWithSubdomains = async (
371
401
  domain: Domain
372
402
  ): Promise<{
@@ -428,6 +458,15 @@ export const getUbiquitousLanguageWithSubdomains = async (
428
458
  };
429
459
  };
430
460
 
461
+ export const hasUbiquitousLanguageTermsWithSubdomains = async (domain: Domain): Promise<boolean> => {
462
+ const { domain: domainUbiquitousLanguage, subdomains } = await getUbiquitousLanguageWithSubdomains(domain);
463
+
464
+ return (
465
+ hasUbiquitousLanguageTerms(domainUbiquitousLanguage) ||
466
+ subdomains.some(({ ubiquitousLanguage }) => hasUbiquitousLanguageTerms(ubiquitousLanguage))
467
+ );
468
+ };
469
+
431
470
  export const getParentDomains = async (domain: Domain): Promise<Domain[]> => {
432
471
  const domains = await getDomains({ getAllVersions: false });
433
472
  return domains.filter((d) => {
@@ -2,6 +2,8 @@ import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import { createVersionedMap, satisfies } from './util';
4
4
 
5
+ const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
6
+
5
7
  export type Entity = CollectionEntry<'entities'>;
6
8
 
7
9
  interface Props {
@@ -15,7 +17,7 @@ export const getEntities = async ({ getAllVersions = true }: Props = {}): Promis
15
17
  // console.time('✅ New getEntities');
16
18
  const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions';
17
19
 
18
- if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0) {
20
+ if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0 && CACHE_ENABLED) {
19
21
  // console.timeEnd('✅ New getEntities');
20
22
  return memoryCache[cacheKey];
21
23
  }
@@ -59,3 +59,12 @@ export const getAbsoluteFilePathForAstroFile = (filePath: string, fileName?: str
59
59
  export const isAvroSchema = (filePath: string): boolean => {
60
60
  return filePath.endsWith('.avro') || filePath.endsWith('.avsc');
61
61
  };
62
+
63
+ /**
64
+ * Checks if a file path is a Protocol Buffers schema based on its extension
65
+ * @param filePath - The file path to check
66
+ * @returns True if the file is a Protocol Buffers schema (.proto)
67
+ */
68
+ export const isProtobufSchema = (filePath: string): boolean => {
69
+ return filePath.endsWith('.proto');
70
+ };
@@ -10,6 +10,18 @@ import { getServices, type Service } from '@utils/collections/services';
10
10
 
11
11
  const elk = new ELK();
12
12
 
13
+ const getReferencedEntityId = (property: any, entityMap: Map<string, Entity[]>) => {
14
+ if (property.references) return property.references;
15
+ if (property.type === 'array' && property.items?.type && entityMap.has(property.items.type)) return property.items.type;
16
+ return undefined;
17
+ };
18
+
19
+ const getRelationType = (property: any) => {
20
+ if (property.relationType) return property.relationType;
21
+ if (property.type === 'array' && property.items?.type) return 'hasMany';
22
+ return 'references';
23
+ };
24
+
13
25
  interface Props {
14
26
  id: string;
15
27
  version: string;
@@ -23,6 +35,7 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
23
35
 
24
36
  // 1. Fetch all collections in parallel
25
37
  const [allDomains, allEntities, allServices] = await Promise.all([getDomains(), getEntities(), getServices()]);
38
+ const entityMap = createVersionedMap(allEntities);
26
39
 
27
40
  let resource = null;
28
41
 
@@ -42,7 +55,7 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
42
55
  }
43
56
 
44
57
  const entitiesWithReferences = resourceEntities.filter((entity: Entity) =>
45
- entity.data.properties?.some((property: any) => property.references)
58
+ entity.data.properties?.some((property: any) => getReferencedEntityId(property, entityMap))
46
59
  );
47
60
  // Creates all the entity nodes for the domain
48
61
  for (const entity of resourceEntities) {
@@ -57,7 +70,7 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
57
70
 
58
71
  // Create entities that are referenced but not owned by this domain
59
72
  const listOfReferencedEntities = entitiesWithReferences
60
- .map((entity: Entity) => entity.data.properties?.map((property: any) => property.references))
73
+ .map((entity: Entity) => entity.data.properties?.map((property: any) => getReferencedEntityId(property, entityMap)))
61
74
  .flat()
62
75
  .filter((ref: any) => ref !== undefined);
63
76
 
@@ -67,8 +80,6 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
67
80
  // 2. Build optimized maps
68
81
  // Only build domain map if we have domains to search
69
82
  // Only build entity map if we have entities to search
70
- const entityMap = createVersionedMap(allEntities);
71
-
72
83
  // Helper function to find which domain an entity belongs to
73
84
  // Optimized to use direct iteration over domains (domains usually contain entity arrays)
74
85
  // We can't easily map entity->domain without scanning domains first unless we build a reverse index.
@@ -123,12 +134,15 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
123
134
  // Go through any entities that are related to other entities
124
135
  for (const entity of entitiesWithReferences) {
125
136
  // Get a list of properties that reference other entities
126
- const allReferencesForEntity = entity.data.properties?.filter((property: any) => property.references) ?? [];
137
+ const allReferencesForEntity =
138
+ entity.data.properties?.filter((property: any) => getReferencedEntityId(property, entityMap)) ?? [];
127
139
 
128
140
  for (const referenceProperty of allReferencesForEntity) {
141
+ const referencedEntityId = getReferencedEntityId(referenceProperty, entityMap);
142
+
129
143
  // Find the referenced entity by matching the references field with entity IDs
130
144
  // Look in both domain entities and external entities
131
- const referencedEntity = allEntitiesInGraph.find((targetEntity) => targetEntity.data.id === referenceProperty.references);
145
+ const referencedEntity = allEntitiesInGraph.find((targetEntity) => targetEntity.data.id === referencedEntityId);
132
146
 
133
147
  if (referencedEntity) {
134
148
  const sourceNodeId = generateIdForNode(entity);
@@ -164,7 +178,7 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
164
178
  targetHandle: targetHandle,
165
179
  type: 'animated',
166
180
  animated: true,
167
- label: referenceProperty.relationType || 'references',
181
+ label: getRelationType(referenceProperty),
168
182
  style: {
169
183
  strokeWidth: 2,
170
184
  strokeDasharray: '5,5', // dashed line
@@ -176,9 +190,7 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
176
190
  },
177
191
  });
178
192
  } else {
179
- console.warn(
180
- `Referenced entity "${referenceProperty.references}" not found for ${entity.data.name}.${referenceProperty.name}`
181
- );
193
+ console.warn(`Referenced entity "${referencedEntityId}" not found for ${entity.data.name}.${referenceProperty.name}`);
182
194
  }
183
195
  }
184
196
  }
@@ -188,10 +200,10 @@ export const getNodesAndEdges = async ({ id, version, entities, type = 'domains'
188
200
  // Separate entities with and without relationships (including external entities)
189
201
  const entitiesWithRelationships = allEntitiesInGraph.filter((entity) => {
190
202
  // Has outgoing references
191
- const hasOutgoingRefs = entity.data.properties?.some((property: any) => property.references);
203
+ const hasOutgoingRefs = entity.data.properties?.some((property: any) => getReferencedEntityId(property, entityMap));
192
204
  // Has incoming references (is referenced by others)
193
205
  const hasIncomingRefs = entitiesWithReferences.some((e: any) =>
194
- e.data.properties?.some((prop: any) => prop.references === entity.data.id)
206
+ e.data.properties?.some((prop: any) => getReferencedEntityId(prop, entityMap) === entity.data.id)
195
207
  );
196
208
  return hasOutgoingRefs || hasIncomingRefs;
197
209
  });