@eventcatalog/core 3.45.0 → 3.46.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/{chunk-E4K4KTSR.js → chunk-DOHA5HNJ.js} +1 -1
- package/dist/{chunk-2QK37BNF.js → chunk-FNOYJEUK.js} +1 -1
- package/dist/{chunk-6XNMBNIU.js → chunk-JS6IYB55.js} +1 -1
- package/dist/{chunk-G6KYCMUM.js → chunk-TLLUDBO4.js} +1 -1
- package/dist/{chunk-KHL25572.js → chunk-X4AESI6E.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.config.d.cts +29 -0
- package/dist/eventcatalog.config.d.ts +29 -0
- package/dist/eventcatalog.js +10 -10
- 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 +7 -10
- package/eventcatalog/ec.config.mjs +20 -0
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerPortal.tsx +1 -1
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +19 -47
- package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +53 -0
- package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.ts +133 -0
- package/eventcatalog/src/content.config.ts +7 -0
- package/eventcatalog/src/enterprise/tools/catalog-tools.ts +23 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -1
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +25 -2
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +1 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +3 -0
- package/eventcatalog/src/utils/collections/schema-loader.ts +302 -16
- package/eventcatalog/src/utils/files.ts +1 -1
- package/package.json +3 -3
|
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
140
140
|
var import_os = __toESM(require("os"), 1);
|
|
141
141
|
|
|
142
142
|
// package.json
|
|
143
|
-
var version = "3.
|
|
143
|
+
var version = "3.46.0";
|
|
144
144
|
|
|
145
145
|
// src/constants.ts
|
|
146
146
|
var VERSION = version;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-FNOYJEUK.js";
|
|
4
|
+
import "../chunk-JS6IYB55.js";
|
|
5
5
|
import "../chunk-DAOXTQVS.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-DOHA5HNJ.js";
|
|
7
7
|
import "../chunk-6QENHZZP.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
144
144
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
145
145
|
|
|
146
146
|
// package.json
|
|
147
|
-
var version = "3.
|
|
147
|
+
var version = "3.46.0";
|
|
148
148
|
|
|
149
149
|
// src/constants.ts
|
|
150
150
|
var VERSION = version;
|
|
@@ -58,6 +58,28 @@ type DirectorySource = {
|
|
|
58
58
|
loadUsers?: () => Promise<DirectoryEntry[]>;
|
|
59
59
|
loadTeams?: () => Promise<DirectoryEntry[]>;
|
|
60
60
|
};
|
|
61
|
+
type SchemaEntry = {
|
|
62
|
+
id: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
format?: string;
|
|
65
|
+
content: string;
|
|
66
|
+
source: {
|
|
67
|
+
provider: string;
|
|
68
|
+
id?: string;
|
|
69
|
+
url?: string;
|
|
70
|
+
ref?: string;
|
|
71
|
+
path?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
type SchemaSource = {
|
|
76
|
+
type: 'schemas';
|
|
77
|
+
name: string;
|
|
78
|
+
canResolve: (id: string) => boolean;
|
|
79
|
+
resolve: (id: string, context?: {
|
|
80
|
+
messageFilePath?: string;
|
|
81
|
+
}) => Promise<SchemaEntry | undefined>;
|
|
82
|
+
};
|
|
61
83
|
type DirectoryConfig = {
|
|
62
84
|
/**
|
|
63
85
|
* External sources that sync users and teams into EventCatalog.
|
|
@@ -71,6 +93,12 @@ type DirectoryConfig = {
|
|
|
71
93
|
*/
|
|
72
94
|
conflictStrategy?: 'local-wins' | 'source-wins' | 'error';
|
|
73
95
|
};
|
|
96
|
+
type SchemaConfig = {
|
|
97
|
+
/**
|
|
98
|
+
* External sources that resolve message schema references into the generated schemas collection.
|
|
99
|
+
*/
|
|
100
|
+
sources?: SchemaSource[];
|
|
101
|
+
};
|
|
74
102
|
type AuthConfig = {
|
|
75
103
|
enabled: boolean;
|
|
76
104
|
};
|
|
@@ -221,6 +249,7 @@ interface Config {
|
|
|
221
249
|
domains?: ResourceDependency[];
|
|
222
250
|
};
|
|
223
251
|
directory?: DirectoryConfig;
|
|
252
|
+
schemas?: SchemaConfig;
|
|
224
253
|
mermaid?: {
|
|
225
254
|
maxTextSize?: number;
|
|
226
255
|
iconPacks?: string[];
|
|
@@ -58,6 +58,28 @@ type DirectorySource = {
|
|
|
58
58
|
loadUsers?: () => Promise<DirectoryEntry[]>;
|
|
59
59
|
loadTeams?: () => Promise<DirectoryEntry[]>;
|
|
60
60
|
};
|
|
61
|
+
type SchemaEntry = {
|
|
62
|
+
id: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
format?: string;
|
|
65
|
+
content: string;
|
|
66
|
+
source: {
|
|
67
|
+
provider: string;
|
|
68
|
+
id?: string;
|
|
69
|
+
url?: string;
|
|
70
|
+
ref?: string;
|
|
71
|
+
path?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
type SchemaSource = {
|
|
76
|
+
type: 'schemas';
|
|
77
|
+
name: string;
|
|
78
|
+
canResolve: (id: string) => boolean;
|
|
79
|
+
resolve: (id: string, context?: {
|
|
80
|
+
messageFilePath?: string;
|
|
81
|
+
}) => Promise<SchemaEntry | undefined>;
|
|
82
|
+
};
|
|
61
83
|
type DirectoryConfig = {
|
|
62
84
|
/**
|
|
63
85
|
* External sources that sync users and teams into EventCatalog.
|
|
@@ -71,6 +93,12 @@ type DirectoryConfig = {
|
|
|
71
93
|
*/
|
|
72
94
|
conflictStrategy?: 'local-wins' | 'source-wins' | 'error';
|
|
73
95
|
};
|
|
96
|
+
type SchemaConfig = {
|
|
97
|
+
/**
|
|
98
|
+
* External sources that resolve message schema references into the generated schemas collection.
|
|
99
|
+
*/
|
|
100
|
+
sources?: SchemaSource[];
|
|
101
|
+
};
|
|
74
102
|
type AuthConfig = {
|
|
75
103
|
enabled: boolean;
|
|
76
104
|
};
|
|
@@ -221,6 +249,7 @@ interface Config {
|
|
|
221
249
|
domains?: ResourceDependency[];
|
|
222
250
|
};
|
|
223
251
|
directory?: DirectoryConfig;
|
|
252
|
+
schemas?: SchemaConfig;
|
|
224
253
|
mermaid?: {
|
|
225
254
|
maxTextSize?: number;
|
|
226
255
|
iconPacks?: string[];
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
2
|
+
log_build_default
|
|
3
|
+
} from "./chunk-FNOYJEUK.js";
|
|
4
|
+
import "./chunk-JS6IYB55.js";
|
|
5
|
+
import "./chunk-DAOXTQVS.js";
|
|
5
6
|
import {
|
|
6
7
|
resolve_catalog_dependencies_default
|
|
7
8
|
} from "./chunk-LHR4G2UO.js";
|
|
@@ -12,10 +13,9 @@ import {
|
|
|
12
13
|
watch
|
|
13
14
|
} from "./chunk-3H2RT3CM.js";
|
|
14
15
|
import {
|
|
15
|
-
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
18
|
-
import "./chunk-DAOXTQVS.js";
|
|
16
|
+
runMigrations
|
|
17
|
+
} from "./chunk-XUAF2H54.js";
|
|
18
|
+
import "./chunk-CA4U2JP7.js";
|
|
19
19
|
import {
|
|
20
20
|
catalogToAstro
|
|
21
21
|
} from "./chunk-4SMA4HQ3.js";
|
|
@@ -28,13 +28,13 @@ import {
|
|
|
28
28
|
} from "./chunk-B7HCX5HM.js";
|
|
29
29
|
import {
|
|
30
30
|
generate
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-X4AESI6E.js";
|
|
32
32
|
import {
|
|
33
33
|
logger
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-TLLUDBO4.js";
|
|
35
35
|
import {
|
|
36
36
|
VERSION
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-DOHA5HNJ.js";
|
|
38
38
|
import {
|
|
39
39
|
getEventCatalogConfigFile,
|
|
40
40
|
verifyRequiredFieldsAreInCatalogConfigFile
|
package/dist/generate.cjs
CHANGED
|
@@ -108,7 +108,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
|
|
|
108
108
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
109
109
|
|
|
110
110
|
// package.json
|
|
111
|
-
var version = "3.
|
|
111
|
+
var version = "3.46.0";
|
|
112
112
|
|
|
113
113
|
// src/constants.ts
|
|
114
114
|
var VERSION = version;
|
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-X4AESI6E.js";
|
|
4
|
+
import "./chunk-TLLUDBO4.js";
|
|
5
|
+
import "./chunk-DOHA5HNJ.js";
|
|
6
6
|
import "./chunk-6QENHZZP.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -19,6 +19,11 @@ import rehypeExpressiveCode from 'rehype-expressive-code';
|
|
|
19
19
|
/** @type {import('bin/eventcatalog.config').Config} */
|
|
20
20
|
import config from './eventcatalog.config';
|
|
21
21
|
import expressiveCode from 'astro-expressive-code';
|
|
22
|
+
// Expressive Code options (including the non-serializable `themeCssSelector`
|
|
23
|
+
// function) live in ec.config.mjs. The `expressiveCode()` integration loads that
|
|
24
|
+
// file automatically; the rehype plugin used by mdx() below needs it passed
|
|
25
|
+
// explicitly, so we import it here to keep a single source of truth.
|
|
26
|
+
import expressiveCodeConfig from './ec.config.mjs';
|
|
22
27
|
import ecstudioWatcher from './integrations/ecstudio-watcher.mjs';
|
|
23
28
|
import eventCatalogIntegration from './src/enterprise/integrations/eventcatalog-features.ts';
|
|
24
29
|
|
|
@@ -30,13 +35,6 @@ const isDevMode = process.env.EVENTCATALOG_DEV_MODE === 'true';
|
|
|
30
35
|
const effectiveOutput = isDevMode ? 'server' : config.output || 'static';
|
|
31
36
|
const searchType = config.search?.type || 'resource';
|
|
32
37
|
|
|
33
|
-
const expressiveCodeConfig = {
|
|
34
|
-
themes: ['github-light', 'github-dark'],
|
|
35
|
-
defaultProps: {
|
|
36
|
-
wrap: true,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
38
|
const markdownRemarkPlugins = [remarkDirective, remarkDirectives, remarkComment, mermaid, plantuml];
|
|
41
39
|
const mdxRemarkPlugins = [...markdownRemarkPlugins, remarkResourceRef];
|
|
42
40
|
|
|
@@ -78,9 +76,8 @@ export default defineConfig({
|
|
|
78
76
|
devToolbar: { enabled: false },
|
|
79
77
|
integrations: [
|
|
80
78
|
react(),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}),
|
|
79
|
+
// Options are loaded automatically from ec.config.mjs.
|
|
80
|
+
expressiveCode(),
|
|
84
81
|
mdx({
|
|
85
82
|
// https://docs.astro.build/en/guides/integrations-guide/mdx/#optimize
|
|
86
83
|
optimize: config.mdxOptimize || false,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineEcConfig } from 'astro-expressive-code';
|
|
2
|
+
|
|
3
|
+
// Expressive Code configuration lives in this dedicated file (rather than inline
|
|
4
|
+
// in astro.config.mjs) because it contains non-serializable options such as the
|
|
5
|
+
// `themeCssSelector` function. Astro's `<Code>` component (used on the print
|
|
6
|
+
// pages) requires Expressive Code options coming from the Astro config to be
|
|
7
|
+
// JSON-serializable, so functions must be defined here instead.
|
|
8
|
+
//
|
|
9
|
+
// `useDarkModeMediaQuery: false` + `themeCssSelector` make code blocks follow
|
|
10
|
+
// EventCatalog's own `data-theme` toggle instead of the OS `prefers-color-scheme`
|
|
11
|
+
// media query, so they stay readable when the catalog theme and the OS
|
|
12
|
+
// preference disagree (see issue #2607).
|
|
13
|
+
export default defineEcConfig({
|
|
14
|
+
themes: ['github-light', 'github-dark'],
|
|
15
|
+
useDarkModeMediaQuery: false,
|
|
16
|
+
themeCssSelector: (theme) => `[data-theme='${theme.type}']`,
|
|
17
|
+
defaultProps: {
|
|
18
|
+
wrap: true,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -5,7 +5,7 @@ const SchemaViewerPortal = (props: any) => {
|
|
|
5
5
|
|
|
6
6
|
return (
|
|
7
7
|
<div
|
|
8
|
-
id={
|
|
8
|
+
data-schema-viewer-portal-id={props.id}
|
|
9
9
|
data-expand={expandBool ? 'true' : 'false'}
|
|
10
10
|
data-max-height={props.maxHeight}
|
|
11
11
|
data-search={searchBool ? 'true' : 'false'}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
const { id, filePath } = Astro.props;
|
|
2
|
+
const { id, filePath, collection, version } = Astro.props;
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
-
import {
|
|
5
|
-
import yaml from 'js-yaml';
|
|
4
|
+
import { getCollection } from 'astro:content';
|
|
6
5
|
import JSONSchemaViewer from '@components/SchemaExplorer/JSONSchemaViewer';
|
|
7
6
|
import AvroSchemaViewer from '@components/SchemaExplorer/AvroSchemaViewer';
|
|
8
7
|
import Admonition from '../Admonition';
|
|
9
8
|
import { getMDXComponentsByName } from '@utils/markdown';
|
|
10
|
-
import {
|
|
9
|
+
import { resolveProjectPath } from '@utils/files';
|
|
10
|
+
import { resolveSchemaViewer } from './schema-viewer-utils';
|
|
11
11
|
|
|
12
12
|
let schemas = [];
|
|
13
13
|
|
|
@@ -15,45 +15,11 @@ try {
|
|
|
15
15
|
const absoluteFilePath = resolveProjectPath(filePath);
|
|
16
16
|
const file = await fs.readFile(absoluteFilePath, 'utf-8');
|
|
17
17
|
const schemaViewers = getMDXComponentsByName(file, 'SchemaViewer');
|
|
18
|
+
const collectionSchemas = collection && version ? await getCollection('schemas') : [];
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const exists = existsSync(schemaPath);
|
|
23
|
-
let schema;
|
|
24
|
-
let render = true;
|
|
25
|
-
let isAvro = false;
|
|
26
|
-
|
|
27
|
-
if (exists) {
|
|
28
|
-
// Check if this is an Avro schema file
|
|
29
|
-
isAvro = isAvroSchema(schemaPath);
|
|
30
|
-
|
|
31
|
-
// Load the schema for the component
|
|
32
|
-
schema = await fs.readFile(schemaPath, 'utf-8');
|
|
33
|
-
|
|
34
|
-
if (schemaPath.endsWith('.yml') || schemaPath.endsWith('.yaml')) {
|
|
35
|
-
schema = yaml.load(schema);
|
|
36
|
-
} else {
|
|
37
|
-
schema = JSON.parse(schema);
|
|
38
|
-
|
|
39
|
-
// For non-Avro schemas, let JSON schema control if the component should be rendered
|
|
40
|
-
if (!isAvro && schema['x-eventcatalog-render-schema-viewer'] !== undefined) {
|
|
41
|
-
render = schema['x-eventcatalog-render-schema-viewer'];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
id: schemaViewerProps.id || id,
|
|
48
|
-
exists,
|
|
49
|
-
schema,
|
|
50
|
-
schemaPath,
|
|
51
|
-
isAvroSchema: isAvro,
|
|
52
|
-
...schemaViewerProps,
|
|
53
|
-
render,
|
|
54
|
-
index,
|
|
55
|
-
};
|
|
56
|
-
});
|
|
20
|
+
const getAllComponents = schemaViewers.map((schemaViewerProps: any, index: number) =>
|
|
21
|
+
resolveSchemaViewer({ id, version, collection, filePath, schemaViewerProps, collectionSchemas, index })
|
|
22
|
+
);
|
|
57
23
|
|
|
58
24
|
schemas = await Promise.all(getAllComponents);
|
|
59
25
|
} catch (error) {
|
|
@@ -71,8 +37,10 @@ try {
|
|
|
71
37
|
<div>
|
|
72
38
|
{schema.exists && (
|
|
73
39
|
<div
|
|
74
|
-
id={`${schema.id}-${schema.
|
|
40
|
+
id={`${schema.id}-${schema.index}-SchemaViewer-client`}
|
|
75
41
|
class="not-prose my-4"
|
|
42
|
+
data-schema-viewer-client-id={schema.id}
|
|
43
|
+
data-schema-viewer-index={schema.index}
|
|
76
44
|
data-expand={schema.expand ? 'true' : 'false'}
|
|
77
45
|
data-search={schema.search !== false ? 'true' : 'false'}
|
|
78
46
|
>
|
|
@@ -105,7 +73,11 @@ try {
|
|
|
105
73
|
<Admonition type="warning">
|
|
106
74
|
<div>
|
|
107
75
|
<span class="block font-bold">{`<SchemaViewer/>`} failed to load</span>
|
|
108
|
-
<span class="block">
|
|
76
|
+
<span class="block">
|
|
77
|
+
{schema.parseError
|
|
78
|
+
? `Tried to parse schema from ${schema.schemaPath}, but it failed: ${schema.parseError}`
|
|
79
|
+
: `Tried to load schema from ${schema.schemaPath || schema.file || 'the message schema collection'}, but no schema can be found`}
|
|
80
|
+
</span>
|
|
109
81
|
</div>
|
|
110
82
|
</Admonition>
|
|
111
83
|
)}
|
|
@@ -120,9 +92,9 @@ try {
|
|
|
120
92
|
// and then we can move the SchemaViewerClient to that container?
|
|
121
93
|
|
|
122
94
|
function moveSchemaViewerToPortal(schema) {
|
|
123
|
-
const
|
|
124
|
-
const schemaViewerContainer =
|
|
125
|
-
const schemaViewerClient = document.getElementById(`${schema.id}-${schema.
|
|
95
|
+
const schemaViewerContainers = document.querySelectorAll(`[data-schema-viewer-portal-id="${schema.id}"]`);
|
|
96
|
+
const schemaViewerContainer = schemaViewerContainers[schema.index];
|
|
97
|
+
const schemaViewerClient = document.getElementById(`${schema.id}-${schema.index}-SchemaViewer-client`);
|
|
126
98
|
|
|
127
99
|
if (schemaViewerContainer && schemaViewerClient) {
|
|
128
100
|
// Get attributes from the portal
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { resolveSchemaViewer } from './schema-viewer-utils';
|
|
3
|
+
|
|
4
|
+
const currentMessageSchema = {
|
|
5
|
+
id: 'schema:events:OrderPlaced:1.0.0:git://contracts/events/OrderPlaced.schema.json',
|
|
6
|
+
data: {
|
|
7
|
+
ref: 'git://contracts/events/OrderPlaced.schema.json',
|
|
8
|
+
format: 'jsonschema',
|
|
9
|
+
content: '{"type":"object","title":"OrderPlaced"}',
|
|
10
|
+
default: true,
|
|
11
|
+
message: {
|
|
12
|
+
collection: 'events',
|
|
13
|
+
id: 'OrderPlaced',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('resolveSchemaViewer', () => {
|
|
20
|
+
it('uses the current message default schema when no file is set', async () => {
|
|
21
|
+
const schema = await resolveSchemaViewer({
|
|
22
|
+
id: 'OrderPlaced',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
collection: 'events',
|
|
25
|
+
filePath: 'events/OrderPlaced/index.mdx',
|
|
26
|
+
schemaViewerProps: {},
|
|
27
|
+
collectionSchemas: [currentMessageSchema],
|
|
28
|
+
index: 0,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(schema.exists).toBe(true);
|
|
32
|
+
expect(schema.schema.title).toBe('OrderPlaced');
|
|
33
|
+
expect(schema.schemaPath).toBe('git://contracts/events/OrderPlaced.schema.json');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not use the schema collection when a file is set and missing', async () => {
|
|
37
|
+
const schema = await resolveSchemaViewer({
|
|
38
|
+
id: 'OrderPlaced',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
collection: 'events',
|
|
41
|
+
filePath: 'events/OrderPlaced/index.mdx',
|
|
42
|
+
schemaViewerProps: {
|
|
43
|
+
file: 'missing-schema.json',
|
|
44
|
+
},
|
|
45
|
+
collectionSchemas: [currentMessageSchema],
|
|
46
|
+
index: 0,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(schema.exists).toBe(false);
|
|
50
|
+
expect(schema.schema).toBeUndefined();
|
|
51
|
+
expect(schema.schemaKey).toBe('missing-schema.json');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { load as loadYaml } from 'js-yaml';
|
|
4
|
+
import { getAbsoluteFilePathForAstroFile, isAvroSchema } from '../../../utils/files';
|
|
5
|
+
|
|
6
|
+
type SchemaViewerProps = {
|
|
7
|
+
id?: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type CollectionSchema = {
|
|
13
|
+
id: string;
|
|
14
|
+
data: {
|
|
15
|
+
content?: string;
|
|
16
|
+
file?: string;
|
|
17
|
+
filePath?: string;
|
|
18
|
+
ref?: string;
|
|
19
|
+
format?: string;
|
|
20
|
+
default?: boolean;
|
|
21
|
+
source?: {
|
|
22
|
+
path?: string;
|
|
23
|
+
branch?: string;
|
|
24
|
+
};
|
|
25
|
+
message?: {
|
|
26
|
+
collection: string;
|
|
27
|
+
id: string;
|
|
28
|
+
version: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ResolveSchemaViewerOptions = {
|
|
34
|
+
id: string;
|
|
35
|
+
version?: string;
|
|
36
|
+
collection?: string;
|
|
37
|
+
filePath: string;
|
|
38
|
+
schemaViewerProps: SchemaViewerProps;
|
|
39
|
+
collectionSchemas: CollectionSchema[];
|
|
40
|
+
index: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getSchemaViewerKey = (schemaViewerProps: SchemaViewerProps) => schemaViewerProps.file || 'default';
|
|
44
|
+
|
|
45
|
+
const isYamlSchema = (schemaPath?: string, format?: string) =>
|
|
46
|
+
schemaPath?.endsWith('.yml') || schemaPath?.endsWith('.yaml') || format === 'yaml';
|
|
47
|
+
|
|
48
|
+
const isAvroSchemaReference = (schemaPath?: string, format?: string) =>
|
|
49
|
+
format === 'avro' || (schemaPath ? isAvroSchema(schemaPath) : false);
|
|
50
|
+
|
|
51
|
+
const parseSchemaContent = (content: string, schemaPath?: string, format?: string) => {
|
|
52
|
+
if (isYamlSchema(schemaPath, format)) return loadYaml(content);
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const shouldRenderSchema = (schema: any, isAvro: boolean) => {
|
|
57
|
+
if (isAvro) return true;
|
|
58
|
+
return schema?.['x-eventcatalog-render-schema-viewer'] !== undefined ? schema['x-eventcatalog-render-schema-viewer'] : true;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const getCollectionSchemaForViewer = ({
|
|
62
|
+
collectionSchemas,
|
|
63
|
+
collection,
|
|
64
|
+
id,
|
|
65
|
+
version,
|
|
66
|
+
}: Pick<ResolveSchemaViewerOptions, 'collectionSchemas' | 'collection' | 'id' | 'version'>) => {
|
|
67
|
+
if (!collection || !version) return undefined;
|
|
68
|
+
|
|
69
|
+
const resourceSchemas = collectionSchemas.filter((schema) => {
|
|
70
|
+
const message = schema.data.message;
|
|
71
|
+
if (!message) return false;
|
|
72
|
+
return message.collection === collection && message.id === id && message.version === version;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return resourceSchemas.find((schema) => schema.data.default) || resourceSchemas[0];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getCollectionSchemaPath = (schema?: CollectionSchema) =>
|
|
79
|
+
schema?.data.filePath || schema?.data.file || schema?.data.source?.path || schema?.data.ref;
|
|
80
|
+
|
|
81
|
+
export const resolveSchemaViewer = async ({
|
|
82
|
+
id,
|
|
83
|
+
version,
|
|
84
|
+
collection,
|
|
85
|
+
filePath,
|
|
86
|
+
schemaViewerProps,
|
|
87
|
+
collectionSchemas,
|
|
88
|
+
index,
|
|
89
|
+
}: ResolveSchemaViewerOptions) => {
|
|
90
|
+
const schemaKey = getSchemaViewerKey(schemaViewerProps);
|
|
91
|
+
const localSchemaPath = schemaViewerProps.file ? getAbsoluteFilePathForAstroFile(filePath, schemaViewerProps.file) : undefined;
|
|
92
|
+
const localSchemaExists = localSchemaPath ? existsSync(localSchemaPath) : false;
|
|
93
|
+
|
|
94
|
+
let schema;
|
|
95
|
+
let render = true;
|
|
96
|
+
let isAvro = false;
|
|
97
|
+
let schemaPath = localSchemaPath;
|
|
98
|
+
let parseError;
|
|
99
|
+
|
|
100
|
+
if (localSchemaExists && localSchemaPath) {
|
|
101
|
+
isAvro = isAvroSchema(localSchemaPath);
|
|
102
|
+
const content = await readFile(localSchemaPath, 'utf-8');
|
|
103
|
+
schema = parseSchemaContent(content, localSchemaPath);
|
|
104
|
+
render = shouldRenderSchema(schema, isAvro);
|
|
105
|
+
} else if (!schemaViewerProps.file) {
|
|
106
|
+
const collectionSchema = getCollectionSchemaForViewer({ collectionSchemas, collection, id, version });
|
|
107
|
+
const content = collectionSchema?.data.content;
|
|
108
|
+
schemaPath = getCollectionSchemaPath(collectionSchema);
|
|
109
|
+
|
|
110
|
+
if (content) {
|
|
111
|
+
try {
|
|
112
|
+
isAvro = isAvroSchemaReference(schemaPath, collectionSchema?.data.format);
|
|
113
|
+
schema = parseSchemaContent(content, schemaPath, collectionSchema?.data.format);
|
|
114
|
+
render = shouldRenderSchema(schema, isAvro);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
parseError = error instanceof Error ? error.message : 'Unknown parsing error';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
id: schemaViewerProps.id || id,
|
|
123
|
+
exists: localSchemaExists || schema !== undefined,
|
|
124
|
+
schema,
|
|
125
|
+
schemaPath,
|
|
126
|
+
schemaKey,
|
|
127
|
+
isAvroSchema: isAvro,
|
|
128
|
+
parseError,
|
|
129
|
+
...schemaViewerProps,
|
|
130
|
+
render,
|
|
131
|
+
index,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
@@ -71,6 +71,7 @@ const channelPointer = z
|
|
|
71
71
|
|
|
72
72
|
const schemaPointer = z.object({
|
|
73
73
|
id: z.string().optional(),
|
|
74
|
+
ref: z.string().optional(),
|
|
74
75
|
file: z.string().optional(),
|
|
75
76
|
path: z.string().optional(),
|
|
76
77
|
name: z.string().optional(),
|
|
@@ -1018,10 +1019,13 @@ const schemas = defineCollection({
|
|
|
1018
1019
|
]) as string[],
|
|
1019
1020
|
base: projectDirBase,
|
|
1020
1021
|
},
|
|
1022
|
+
sources: config.schemas?.sources ?? [],
|
|
1021
1023
|
}),
|
|
1022
1024
|
schema: z
|
|
1023
1025
|
.object({
|
|
1024
1026
|
format: z.string(),
|
|
1027
|
+
schemaId: z.string().optional(),
|
|
1028
|
+
ref: z.string().optional(),
|
|
1025
1029
|
content: z.string().optional(),
|
|
1026
1030
|
file: z.string().optional(),
|
|
1027
1031
|
filePath: z.string().optional(),
|
|
@@ -1038,8 +1042,11 @@ const schemas = defineCollection({
|
|
|
1038
1042
|
}),
|
|
1039
1043
|
source: z.object({
|
|
1040
1044
|
provider: z.string(),
|
|
1045
|
+
id: z.string().optional(),
|
|
1041
1046
|
path: z.string().optional(),
|
|
1042
1047
|
url: z.string().optional(),
|
|
1048
|
+
ref: z.string().optional(),
|
|
1049
|
+
branch: z.string().optional(),
|
|
1043
1050
|
}),
|
|
1044
1051
|
readOnly: z.boolean().optional(),
|
|
1045
1052
|
})
|
|
@@ -28,6 +28,8 @@ import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-gr
|
|
|
28
28
|
import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
|
|
29
29
|
import config from '@config';
|
|
30
30
|
|
|
31
|
+
const MESSAGE_COLLECTIONS = new Set(['events', 'commands', 'queries']);
|
|
32
|
+
|
|
31
33
|
// ============================================
|
|
32
34
|
// Pagination utilities
|
|
33
35
|
// ============================================
|
|
@@ -226,6 +228,27 @@ export async function getSchemaForResource(params: { resourceId: string; resourc
|
|
|
226
228
|
};
|
|
227
229
|
}
|
|
228
230
|
|
|
231
|
+
if (MESSAGE_COLLECTIONS.has(params.resourceCollection)) {
|
|
232
|
+
const schemaEntries = await getCollection('schemas');
|
|
233
|
+
const schemas = schemaEntries.filter(
|
|
234
|
+
(schema) =>
|
|
235
|
+
schema.data.message.collection === params.resourceCollection &&
|
|
236
|
+
schema.data.message.id === params.resourceId &&
|
|
237
|
+
schema.data.message.version === params.resourceVersion
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (schemas.length > 0) {
|
|
241
|
+
return schemas.map((schema) => ({
|
|
242
|
+
id: schema.id,
|
|
243
|
+
name: schema.data.name,
|
|
244
|
+
ref: schema.data.ref,
|
|
245
|
+
format: schema.data.format,
|
|
246
|
+
source: schema.data.source,
|
|
247
|
+
code: schema.data.content || '',
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
229
252
|
const schema = await getSchemasFromResource(resource);
|
|
230
253
|
|
|
231
254
|
if (schema.length > 0) {
|
|
@@ -565,7 +565,7 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
|
|
|
565
565
|
<div data-pagefind-ignore>
|
|
566
566
|
<!-- @ts-ignore -->
|
|
567
567
|
<!-- <SchemaViewer id={props.data.id} catalog={props.catalog} filePath={props.filePath} /> -->
|
|
568
|
-
<SchemaViewer id={props.data.id} filePath={props.filePath} />
|
|
568
|
+
<SchemaViewer id={props.data.id} version={props.data.version} collection={props.collection} filePath={props.filePath} />
|
|
569
569
|
|
|
570
570
|
{
|
|
571
571
|
nodeGraphs.length > 0 &&
|
|
@@ -15,11 +15,33 @@ import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
|
|
|
15
15
|
import { iconFieldsForResource } from '@utils/icon';
|
|
16
16
|
import { collectionToResourceMap } from '@utils/collections/util';
|
|
17
17
|
|
|
18
|
+
type MessageSchemaEntry = CollectionEntry<'schemas'>;
|
|
19
|
+
|
|
18
20
|
const getProducerConsumerPageRef = (resource: any) => {
|
|
19
21
|
const resourceType = collectionToResourceMap[resource.collection as keyof typeof collectionToResourceMap];
|
|
20
22
|
return `${resourceType}:${resource.data.id}:${resource.data.version}`;
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
const getSchemasForMessage = (
|
|
26
|
+
message: CollectionEntry<'events' | 'commands' | 'queries'>,
|
|
27
|
+
schemas: MessageSchemaEntry[] = []
|
|
28
|
+
) => {
|
|
29
|
+
return schemas.filter(
|
|
30
|
+
(schema) =>
|
|
31
|
+
schema.data.message.collection === message.collection &&
|
|
32
|
+
schema.data.message.id === message.data.id &&
|
|
33
|
+
schema.data.message.version === message.data.version
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getSchemaNavTitle = (schemas: MessageSchemaEntry[]) => {
|
|
38
|
+
if (schemas.length > 1) return 'Schemas';
|
|
39
|
+
|
|
40
|
+
const schemaPath = schemas[0]?.data.file || schemas[0]?.data.source.path || schemas[0]?.data.ref || schemas[0]?.id;
|
|
41
|
+
const format = schemaPath ? getSchemaFormatFromURL(schemaPath) : schemas[0]?.data.format;
|
|
42
|
+
return format ? `Schema (${format.toUpperCase()})` : 'Schema';
|
|
43
|
+
};
|
|
44
|
+
|
|
23
45
|
export const buildMessageNode = (
|
|
24
46
|
message: CollectionEntry<'events' | 'commands' | 'queries'>,
|
|
25
47
|
owners: any[],
|
|
@@ -51,7 +73,8 @@ export const buildMessageNode = (
|
|
|
51
73
|
};
|
|
52
74
|
const defaultIcon = iconMap[collection] || 'Mail';
|
|
53
75
|
|
|
54
|
-
const
|
|
76
|
+
const resolvedSchemas = getSchemasForMessage(message, context.schemas);
|
|
77
|
+
const hasSchema = resolvedSchemas.length > 0;
|
|
55
78
|
const renderVisualiser = isVisualiserEnabled();
|
|
56
79
|
const docsSection = buildResourceDocsSection(
|
|
57
80
|
collection as 'events' | 'commands' | 'queries',
|
|
@@ -116,7 +139,7 @@ export const buildMessageNode = (
|
|
|
116
139
|
pages: [
|
|
117
140
|
{
|
|
118
141
|
type: 'item',
|
|
119
|
-
title:
|
|
142
|
+
title: getSchemaNavTitle(resolvedSchemas),
|
|
120
143
|
href: buildUrl(`/schemas/${collection}/${message.data.id}/${message.data.version}`),
|
|
121
144
|
},
|
|
122
145
|
hasFieldUsage && {
|
|
@@ -67,6 +67,7 @@ export type ResourceGroupContext = {
|
|
|
67
67
|
entities?: CollectionEntry<'entities'>[];
|
|
68
68
|
dataProducts: CollectionEntry<'data-products'>[];
|
|
69
69
|
diagrams: CollectionEntry<'diagrams'>[];
|
|
70
|
+
schemas?: CollectionEntry<'schemas'>[];
|
|
70
71
|
adrs: Adr[];
|
|
71
72
|
users?: CollectionEntry<'users'>[];
|
|
72
73
|
teams?: CollectionEntry<'teams'>[];
|
|
@@ -292,6 +292,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
292
292
|
dataProducts,
|
|
293
293
|
entities,
|
|
294
294
|
adrs,
|
|
295
|
+
schemas,
|
|
295
296
|
resourceDocs,
|
|
296
297
|
resourceDocCategories,
|
|
297
298
|
] = await Promise.all([
|
|
@@ -309,6 +310,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
309
310
|
getDataProducts({ getAllVersions: false }),
|
|
310
311
|
getEntities({ getAllVersions: false }),
|
|
311
312
|
getAdrs({ getAllVersions: false }),
|
|
313
|
+
getCollection('schemas'),
|
|
312
314
|
getResourceDocs(),
|
|
313
315
|
getResourceDocCategories(),
|
|
314
316
|
]);
|
|
@@ -330,6 +332,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
330
332
|
containers,
|
|
331
333
|
channels,
|
|
332
334
|
diagrams,
|
|
335
|
+
schemas,
|
|
333
336
|
dataProducts,
|
|
334
337
|
entities,
|
|
335
338
|
adrs,
|
|
@@ -4,12 +4,18 @@ import matter from 'gray-matter';
|
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import fsSync from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
import { isEventCatalogScaleEnabled } from '../feature';
|
|
7
10
|
import { sortVersioned } from './util';
|
|
8
11
|
|
|
12
|
+
const colors = pc.createColors(true);
|
|
13
|
+
|
|
9
14
|
type MessageCollection = 'events' | 'commands' | 'queries';
|
|
10
15
|
|
|
11
16
|
type SchemaReference = {
|
|
12
17
|
id?: string;
|
|
18
|
+
ref?: string;
|
|
13
19
|
file?: string;
|
|
14
20
|
path?: string;
|
|
15
21
|
name?: string;
|
|
@@ -18,6 +24,34 @@ type SchemaReference = {
|
|
|
18
24
|
default?: boolean;
|
|
19
25
|
};
|
|
20
26
|
|
|
27
|
+
type SchemaSourceEntry = {
|
|
28
|
+
provider: string;
|
|
29
|
+
id?: string;
|
|
30
|
+
path?: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
ref?: string;
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type SchemaSource = {
|
|
37
|
+
type: 'schemas';
|
|
38
|
+
name: string;
|
|
39
|
+
canResolve: (ref: string) => boolean;
|
|
40
|
+
resolve: (
|
|
41
|
+
ref: string,
|
|
42
|
+
context?: { messageFilePath?: string }
|
|
43
|
+
) => Promise<
|
|
44
|
+
| {
|
|
45
|
+
id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
format?: string;
|
|
48
|
+
content: string;
|
|
49
|
+
source: SchemaSourceEntry;
|
|
50
|
+
}
|
|
51
|
+
| undefined
|
|
52
|
+
>;
|
|
53
|
+
};
|
|
54
|
+
|
|
21
55
|
type MessageFrontmatter = {
|
|
22
56
|
id?: string;
|
|
23
57
|
name?: string;
|
|
@@ -30,6 +64,8 @@ type MessageFrontmatter = {
|
|
|
30
64
|
|
|
31
65
|
export type MessageSchemaResource = {
|
|
32
66
|
id: string;
|
|
67
|
+
schemaId?: string;
|
|
68
|
+
ref?: string;
|
|
33
69
|
name?: string;
|
|
34
70
|
version?: string;
|
|
35
71
|
format: string;
|
|
@@ -48,8 +84,19 @@ export type MessageSchemaResource = {
|
|
|
48
84
|
owners?: string[];
|
|
49
85
|
};
|
|
50
86
|
source: {
|
|
51
|
-
provider:
|
|
52
|
-
|
|
87
|
+
provider: string;
|
|
88
|
+
id?: string;
|
|
89
|
+
path?: string;
|
|
90
|
+
url?: string;
|
|
91
|
+
ref?: string;
|
|
92
|
+
[key: string]: unknown;
|
|
93
|
+
};
|
|
94
|
+
readOnly?: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type InternalMessageSchemaResource = MessageSchemaResource & {
|
|
98
|
+
_context?: {
|
|
99
|
+
messageFilePath?: string;
|
|
53
100
|
};
|
|
54
101
|
};
|
|
55
102
|
|
|
@@ -58,11 +105,13 @@ type SchemaLoaderOptions = {
|
|
|
58
105
|
pattern: string[];
|
|
59
106
|
base?: string;
|
|
60
107
|
};
|
|
108
|
+
sources?: SchemaSource[];
|
|
61
109
|
};
|
|
62
110
|
|
|
63
111
|
type LoaderContext = Parameters<Loader['load']>[0];
|
|
64
112
|
|
|
65
113
|
const MESSAGE_COLLECTIONS: MessageCollection[] = ['events', 'commands', 'queries'];
|
|
114
|
+
const FILE_SCHEMA_REF_PREFIX = 'file://';
|
|
66
115
|
|
|
67
116
|
const normalizePath = (value: string) => value.replace(/\\/g, '/');
|
|
68
117
|
|
|
@@ -85,11 +134,137 @@ const getSchemaFormat = (schemaPath: string) => {
|
|
|
85
134
|
const buildGeneratedSchemaId = (message: { collection: MessageCollection; id: string; version: string }, schemaPath: string) =>
|
|
86
135
|
`schema:${message.collection}:${message.id}:${message.version}:${schemaPath}`;
|
|
87
136
|
|
|
137
|
+
const getSchemaCollectionId = ({
|
|
138
|
+
message,
|
|
139
|
+
reference,
|
|
140
|
+
schemaRef,
|
|
141
|
+
schemaFile,
|
|
142
|
+
}: {
|
|
143
|
+
message: { collection: MessageCollection; id: string; version: string };
|
|
144
|
+
reference: SchemaReference;
|
|
145
|
+
schemaRef?: string;
|
|
146
|
+
schemaFile?: string;
|
|
147
|
+
}) => {
|
|
148
|
+
if (schemaRef) return buildGeneratedSchemaId(message, schemaRef);
|
|
149
|
+
if (reference.id) return buildGeneratedSchemaId(message, reference.id);
|
|
150
|
+
return buildGeneratedSchemaId(message, schemaFile as string);
|
|
151
|
+
};
|
|
152
|
+
|
|
88
153
|
const getSchemaFile = (reference: SchemaReference) => reference.file ?? reference.path;
|
|
89
154
|
|
|
155
|
+
const getSchemaRef = (reference: SchemaReference) => {
|
|
156
|
+
if (reference.ref) return reference.ref;
|
|
157
|
+
if (!getSchemaFile(reference)) return reference.id;
|
|
158
|
+
return undefined;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const getSchemaDisplayName = ({
|
|
162
|
+
referenceName,
|
|
163
|
+
resolvedName,
|
|
164
|
+
schemaFile,
|
|
165
|
+
filePath,
|
|
166
|
+
schemaRef,
|
|
167
|
+
sourcePath,
|
|
168
|
+
schemaId,
|
|
169
|
+
messageName,
|
|
170
|
+
}: {
|
|
171
|
+
referenceName?: string;
|
|
172
|
+
resolvedName?: string;
|
|
173
|
+
schemaFile?: string;
|
|
174
|
+
filePath?: string;
|
|
175
|
+
schemaRef?: string;
|
|
176
|
+
sourcePath?: string;
|
|
177
|
+
schemaId: string;
|
|
178
|
+
messageName?: string;
|
|
179
|
+
}) => {
|
|
180
|
+
const pathLikeName = schemaFile ?? filePath ?? sourcePath ?? schemaRef ?? schemaId;
|
|
181
|
+
return referenceName ?? resolvedName ?? path.basename(pathLikeName) ?? messageName ?? 'Schema';
|
|
182
|
+
};
|
|
183
|
+
|
|
90
184
|
const schemaFileExists = (schema: MessageSchemaResource): schema is MessageSchemaResource & { filePath: string } =>
|
|
91
185
|
Boolean(schema.filePath && fsSync.existsSync(schema.filePath));
|
|
92
186
|
|
|
187
|
+
const getSchemaProvider = (id: string) => {
|
|
188
|
+
try {
|
|
189
|
+
const url = new URL(id);
|
|
190
|
+
return url.protocol.replace(':', '') || 'external';
|
|
191
|
+
} catch {
|
|
192
|
+
return 'external';
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const isFileSchemaRef = (ref?: string) => ref?.startsWith(FILE_SCHEMA_REF_PREFIX);
|
|
197
|
+
|
|
198
|
+
const getFileSchemaRefPath = (ref: string, messageFilePath: string) => {
|
|
199
|
+
if (ref.startsWith('file:///') || ref.startsWith('file://localhost/')) {
|
|
200
|
+
try {
|
|
201
|
+
const filePath = fileURLToPath(ref);
|
|
202
|
+
return {
|
|
203
|
+
filePath,
|
|
204
|
+
sourcePath: filePath,
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
throw new Error(`Invalid file schema ref "${ref}". Expected file://<relative-path> or file:///absolute/path.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const sourcePath = decodeURIComponent(ref.slice(FILE_SCHEMA_REF_PREFIX.length));
|
|
212
|
+
if (!sourcePath) {
|
|
213
|
+
throw new Error(`Invalid file schema ref "${ref}". Expected file://<relative-path> or file:///absolute/path.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
filePath: path.resolve(path.dirname(messageFilePath), sourcePath),
|
|
218
|
+
sourcePath,
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const getTimestamp = () => {
|
|
223
|
+
const now = new Date();
|
|
224
|
+
return now.toLocaleTimeString('en-US', { hour12: false });
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const logSchemaInfo = (message: string) => {
|
|
228
|
+
console.log(`${colors.dim(getTimestamp())} ${colors.blue('[schemas]')} ${message}`);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const getMessageTypeLabel = (collection: MessageCollection) => {
|
|
232
|
+
if (collection === 'events') return 'event';
|
|
233
|
+
if (collection === 'commands') return 'command';
|
|
234
|
+
return 'query';
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const getErrorMessage = (error: unknown) => {
|
|
238
|
+
return error instanceof Error ? error.message : String(error);
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const getSchemaResolveRef = (schema: MessageSchemaResource) => schema.ref ?? schema.id;
|
|
242
|
+
|
|
243
|
+
const buildSchemaSourceErrorMessage = ({
|
|
244
|
+
schema,
|
|
245
|
+
source,
|
|
246
|
+
error,
|
|
247
|
+
}: {
|
|
248
|
+
schema: MessageSchemaResource;
|
|
249
|
+
source: SchemaSource;
|
|
250
|
+
error: unknown;
|
|
251
|
+
}) => {
|
|
252
|
+
const messageType = getMessageTypeLabel(schema.message.collection);
|
|
253
|
+
|
|
254
|
+
return [
|
|
255
|
+
'',
|
|
256
|
+
colors.red(colors.bold('[schemas] Failed to resolve schema')),
|
|
257
|
+
'',
|
|
258
|
+
` Message: ${messageType} "${schema.message.id}" version "${schema.message.version}"`,
|
|
259
|
+
` Schema: ${getSchemaResolveRef(schema)}`,
|
|
260
|
+
` Source: ${source.name}`,
|
|
261
|
+
'',
|
|
262
|
+
colors.bold('Reason:'),
|
|
263
|
+
` ${getErrorMessage(error)}`,
|
|
264
|
+
'',
|
|
265
|
+
].join('\n');
|
|
266
|
+
};
|
|
267
|
+
|
|
93
268
|
const addLatestMetadata = (schemas: MessageSchemaResource[]) => {
|
|
94
269
|
const schemasByMessage = schemas.reduce(
|
|
95
270
|
(acc, schema) => {
|
|
@@ -138,11 +313,10 @@ export const getMessageSchemasFromFrontmatter = ({
|
|
|
138
313
|
};
|
|
139
314
|
|
|
140
315
|
const schemaReferences =
|
|
141
|
-
data.schemas?.filter((schema) => getSchemaFile(schema)) ??
|
|
316
|
+
data.schemas?.filter((schema) => getSchemaRef(schema) || getSchemaFile(schema)) ??
|
|
142
317
|
(data.schemaPath
|
|
143
318
|
? [
|
|
144
319
|
{
|
|
145
|
-
id: buildGeneratedSchemaId(message, data.schemaPath),
|
|
146
320
|
file: data.schemaPath,
|
|
147
321
|
name: 'Schema',
|
|
148
322
|
default: true,
|
|
@@ -151,29 +325,123 @@ export const getMessageSchemasFromFrontmatter = ({
|
|
|
151
325
|
: []);
|
|
152
326
|
|
|
153
327
|
return schemaReferences.map((reference) => {
|
|
154
|
-
const schemaFile = getSchemaFile(reference)
|
|
155
|
-
const
|
|
156
|
-
const
|
|
328
|
+
const schemaFile = getSchemaFile(reference);
|
|
329
|
+
const schemaRef = getSchemaRef(reference);
|
|
330
|
+
const fileSchemaRef = isFileSchemaRef(schemaRef) ? getFileSchemaRefPath(schemaRef as string, messageFilePath) : undefined;
|
|
331
|
+
const schemaFilePath = schemaFile ? path.resolve(path.dirname(messageFilePath), schemaFile) : fileSchemaRef?.filePath;
|
|
332
|
+
const schemaId = getSchemaCollectionId({ message, reference, schemaRef, schemaFile });
|
|
333
|
+
const schemaPathForFormat = schemaFile ?? fileSchemaRef?.filePath;
|
|
157
334
|
|
|
158
335
|
return {
|
|
159
336
|
id: schemaId,
|
|
160
|
-
|
|
337
|
+
...(reference.id && !schemaRef ? { schemaId: reference.id } : {}),
|
|
338
|
+
...(schemaRef ? { ref: schemaRef } : {}),
|
|
339
|
+
name: getSchemaDisplayName({
|
|
340
|
+
referenceName: reference.name,
|
|
341
|
+
schemaFile,
|
|
342
|
+
filePath: fileSchemaRef?.filePath,
|
|
343
|
+
schemaRef,
|
|
344
|
+
schemaId,
|
|
345
|
+
messageName: data.name,
|
|
346
|
+
}),
|
|
161
347
|
version: data.version,
|
|
162
|
-
format: reference.format ?? getSchemaFormat(
|
|
348
|
+
format: reference.format ?? (schemaPathForFormat ? getSchemaFormat(schemaPathForFormat) : 'unknown'),
|
|
163
349
|
file: schemaFile,
|
|
164
350
|
filePath: schemaFilePath,
|
|
165
351
|
environments: reference.environments,
|
|
166
352
|
default: reference.default,
|
|
167
353
|
message,
|
|
168
354
|
source: {
|
|
169
|
-
provider: 'file',
|
|
170
|
-
path: schemaFile,
|
|
355
|
+
provider: schemaFile || fileSchemaRef ? 'file' : getSchemaProvider(schemaRef ?? schemaId),
|
|
356
|
+
path: schemaFile ?? fileSchemaRef?.sourcePath,
|
|
171
357
|
},
|
|
172
358
|
};
|
|
173
359
|
});
|
|
174
360
|
};
|
|
175
361
|
|
|
176
|
-
|
|
362
|
+
const resolveSchemaSource = async (
|
|
363
|
+
schema: InternalMessageSchemaResource,
|
|
364
|
+
source: SchemaSource
|
|
365
|
+
): Promise<InternalMessageSchemaResource | undefined> => {
|
|
366
|
+
if (schemaFileExists(schema)) return schema;
|
|
367
|
+
if (schema.filePath) return undefined;
|
|
368
|
+
|
|
369
|
+
let resolvedSchema: Awaited<ReturnType<SchemaSource['resolve']>>;
|
|
370
|
+
const schemaResolveRef = getSchemaResolveRef(schema);
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
resolvedSchema = await source.resolve(schemaResolveRef, schema._context);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(buildSchemaSourceErrorMessage({ schema, source, error }));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!resolvedSchema) return undefined;
|
|
379
|
+
|
|
380
|
+
const resolvedMessageSchema: InternalMessageSchemaResource = {
|
|
381
|
+
...schema,
|
|
382
|
+
format: schema.format !== 'unknown' ? schema.format : (resolvedSchema.format ?? 'unknown'),
|
|
383
|
+
content: resolvedSchema.content,
|
|
384
|
+
source: resolvedSchema.source,
|
|
385
|
+
readOnly: true,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
resolvedMessageSchema.name = getSchemaDisplayName({
|
|
389
|
+
referenceName: schema.name,
|
|
390
|
+
resolvedName: resolvedSchema.name,
|
|
391
|
+
schemaRef: schema.ref,
|
|
392
|
+
sourcePath: resolvedSchema.source.path,
|
|
393
|
+
schemaId: schema.id,
|
|
394
|
+
messageName: schema.message.name,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return resolvedMessageSchema;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const resolveSchemaSources = async (schemas: InternalMessageSchemaResource[], sources: SchemaSource[] = []) => {
|
|
401
|
+
if (sources.length > 0 && !isEventCatalogScaleEnabled()) {
|
|
402
|
+
throw new Error('Schema sources require EventCatalog Scale.');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const localSchemas = schemas.filter(schemaFileExists);
|
|
406
|
+
const externalSchemas = schemas.filter((schema) => !schema.filePath);
|
|
407
|
+
const resolvedExternalSchemas: InternalMessageSchemaResource[] = [];
|
|
408
|
+
const resolvedExternalSchemaIds = new Set<string>();
|
|
409
|
+
|
|
410
|
+
for (const source of sources) {
|
|
411
|
+
const sourceSchemas = externalSchemas.filter((schema) => source.canResolve(getSchemaResolveRef(schema)));
|
|
412
|
+
if (sourceSchemas.length === 0) continue;
|
|
413
|
+
|
|
414
|
+
logSchemaInfo(
|
|
415
|
+
`Loading ${sourceSchemas.length} schema${sourceSchemas.length === 1 ? '' : 's'} from schema source "${source.name}"`
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const resolvedSchemas = await Promise.all(sourceSchemas.map((schema) => resolveSchemaSource(schema, source)));
|
|
419
|
+
const syncedSchemas = resolvedSchemas.filter((schema): schema is InternalMessageSchemaResource => schema !== undefined);
|
|
420
|
+
|
|
421
|
+
for (const schema of syncedSchemas) {
|
|
422
|
+
resolvedExternalSchemaIds.add(schema.id);
|
|
423
|
+
resolvedExternalSchemas.push(schema);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const skippedSchemas = sourceSchemas.length - syncedSchemas.length;
|
|
427
|
+
logSchemaInfo(
|
|
428
|
+
`Synced ${syncedSchemas.length} schema${syncedSchemas.length === 1 ? '' : 's'} from schema source "${source.name}"${
|
|
429
|
+
skippedSchemas > 0 ? ` (${skippedSchemas} skipped)` : ''
|
|
430
|
+
}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return [
|
|
435
|
+
...localSchemas,
|
|
436
|
+
...resolvedExternalSchemas,
|
|
437
|
+
...schemas.filter((schema) => schema.filePath && !schemaFileExists(schema)),
|
|
438
|
+
].filter((schema) => {
|
|
439
|
+
if (schema.filePath) return schemaFileExists(schema);
|
|
440
|
+
return resolvedExternalSchemaIds.has(schema.id);
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const loadMessageSchemaResources = async ({ pattern, base }: SchemaLoaderOptions['messages']) => {
|
|
177
445
|
if (!base) return [];
|
|
178
446
|
|
|
179
447
|
const files = await glob(pattern, {
|
|
@@ -189,14 +457,32 @@ export const loadMessageSchemas = async ({ pattern, base }: SchemaLoaderOptions[
|
|
|
189
457
|
if (!collection) return [];
|
|
190
458
|
|
|
191
459
|
const { data } = matter.read(file) as { data: MessageFrontmatter };
|
|
192
|
-
return getMessageSchemasFromFrontmatter({ data, collection, messageFilePath: file })
|
|
460
|
+
return getMessageSchemasFromFrontmatter({ data, collection, messageFilePath: file }).map((schema) => ({
|
|
461
|
+
...schema,
|
|
462
|
+
_context: {
|
|
463
|
+
messageFilePath: file,
|
|
464
|
+
},
|
|
465
|
+
}));
|
|
193
466
|
})
|
|
194
467
|
);
|
|
195
468
|
|
|
196
|
-
return
|
|
469
|
+
return schemas.flat();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const stripSchemaLoaderContext = (schema: InternalMessageSchemaResource): MessageSchemaResource => {
|
|
473
|
+
const { _context, ...publicSchema } = schema;
|
|
474
|
+
return publicSchema;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
export const loadMessageSchemas = async (messages: SchemaLoaderOptions['messages'], sources: SchemaSource[] = []) => {
|
|
478
|
+
const schemas = await loadMessageSchemaResources(messages);
|
|
479
|
+
const resolvedSchemas = await resolveSchemaSources(schemas, sources);
|
|
480
|
+
|
|
481
|
+
return addLatestMetadata(resolvedSchemas.map(stripSchemaLoaderContext));
|
|
197
482
|
};
|
|
198
483
|
|
|
199
484
|
const getSchemaBody = async (schema: MessageSchemaResource) => {
|
|
485
|
+
if (schema.content !== undefined) return schema.content;
|
|
200
486
|
if (!schemaFileExists(schema)) return undefined;
|
|
201
487
|
return fs.readFile(schema.filePath, 'utf8');
|
|
202
488
|
};
|
|
@@ -222,12 +508,12 @@ const setSchema = async (context: LoaderContext, schema: MessageSchemaResource)
|
|
|
222
508
|
});
|
|
223
509
|
};
|
|
224
510
|
|
|
225
|
-
export const schemaLoader = ({ messages }: SchemaLoaderOptions): Loader => {
|
|
511
|
+
export const schemaLoader = ({ messages, sources = [] }: SchemaLoaderOptions): Loader => {
|
|
226
512
|
return {
|
|
227
513
|
name: 'eventcatalog-schema-loader',
|
|
228
514
|
load: async (context) => {
|
|
229
515
|
context.store.clear();
|
|
230
|
-
const schemas = await loadMessageSchemas(messages);
|
|
516
|
+
const schemas = await loadMessageSchemas(messages, sources);
|
|
231
517
|
|
|
232
518
|
for (const schema of schemas) {
|
|
233
519
|
await setSchema(context, schema);
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "SEE LICENSE IN LICENSE",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "3.
|
|
10
|
+
"version": "3.46.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"elkjs": "^0.10.0",
|
|
75
75
|
"glob": "^13.0.6",
|
|
76
76
|
"gray-matter": "^4.0.3",
|
|
77
|
-
"hono": "4.12.
|
|
77
|
+
"hono": "4.12.21",
|
|
78
78
|
"html-to-image": "^1.11.11",
|
|
79
79
|
"js-yaml": "^4.1.1",
|
|
80
80
|
"jsonpath-plus": "^10.4.0",
|
|
@@ -112,8 +112,8 @@
|
|
|
112
112
|
"update-notifier": "^7.3.1",
|
|
113
113
|
"uuid": "^10.0.0",
|
|
114
114
|
"zod": "^4.3.6",
|
|
115
|
-
"@eventcatalog/sdk": "2.24.1",
|
|
116
115
|
"@eventcatalog/linter": "1.0.29",
|
|
116
|
+
"@eventcatalog/sdk": "2.24.1",
|
|
117
117
|
"@eventcatalog/visualiser": "^3.22.1"
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|